root/src/dps8/dps8_net.c

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

DEFINITIONS

This source file includes following definitions.
  1. net_show_nunits
  2. net_set_nunits
  3. net_show_device_name
  4. net_set_device_name
  5. net_show_socket_path
  6. net_set_socket_path
  7. net_reset
  8. netAttach
  9. netDetach
  10. net_init
  11. net_init_dev_state
  12. net_connect
  13. net_send_packet
  14. net_recv_packet
  15. pkt8_to_word36
  16. word36_to_pkt8
  17. get_ddcw
  18. cmd_name
  19. dumppkt
  20. net_validate_dcw_state
  21. net_cmd
  22. net_iom_cmd
  23. net_check_dma_ptw
  24. net_process_event

   1 /*
   2  * vim: filetype=c:tabstop=4:ai:expandtab
   3  * SPDX-License-Identifier: ICU
   4  * scspell-id: fcc5dfde-ac98-11f1-80b3-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-2025 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 IP packets between the IOM (Multics)
  22 // and an external process (multics_ip) via a Unix domain socket.
  23 //
  24 // The multics_ip process handles unwraps the 1822 leader from packets and
  25 // sends them to or reads them from a TUN device to the Internet.
  26 //
  27 // Communication protocol with the multics_ip process:
  28 //   - Single bidirectional Unix domain socket (default: /tmp/multics_ip)
  29 //   - Length-prefixed framing: [2-byte BE length][encapsulated IP 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 -> multics_ip): Read 36-bit words from IOM, convert to 8-bit,
  33 //   send length-prefixed to multics_ip, return IOM_CMD_DISCONNECT (terminate).
  34 //
  35 // Read (multics_ip -> Multics): Set want_to_read flag, return IOM_CMD_PENDING.
  36 //   In net_process_event(), poll 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 <ctype.h>
  42 #include <unistd.h>
  43 #include <stdint.h>
  44 #include <errno.h>
  45 #include <fcntl.h>
  46 
  47 #include <sys/types.h>
  48 #include <sys/un.h>
  49 #include <sys/select.h>
  50 #include <time.h>
  51 #include <sys/time.h>
  52 #include <sys/socket.h>
  53 
  54 #include "dps8.h"
  55 #include "dps8_sir.h"
  56 #include "dps8_iom.h"
  57 #include "dps8_net.h"
  58 #include "dps8_sys.h"
  59 #include "dps8_cable.h"
  60 #include "dps8_cpu.h"
  61 #include "dps8_faults.h"
  62 #include "dps8_scu.h"
  63 #include "dps8_utils.h"
  64 #include "dps8_uvblock.h"
  65 
  66 #if defined(NO_LOCALE)
  67 # define xstrerror_l strerror
  68 #endif
  69 
  70 #if defined(FREE)
  71 # undef FREE
  72 #endif /* if defined(FREE) */
  73 #define FREE(p) do  \
  74   {                 \
  75     free((p));      \
  76     (p) = NULL;     \
  77   } while(0)
  78 
  79 #if defined(WITH_NET_DEV)
  80 
  81 # define DBG_CTR  1
  82 
  83 // gateway process Unix socket path (configurable via SET NET PATH<path>)
  84 # define GATEWAY_SOCKET_PATH_DEFAULT  "/tmp/multics_ip_gateway"
  85 # define GATEWAY_SOCKET_PATH_MAX      108
  86 
  87 static char gateway_socket_path[GATEWAY_SOCKET_PATH_MAX] = GATEWAY_SOCKET_PATH_DEFAULT;
  88 
  89 // Number of words in a packet header (3 words x 36 bits = 12 x 9-bit bytes)
  90 // The ABSI packet header is the 96-bit (12-byte) IMP 1822L leader.
  91 # define GATEWAY_PACKET_HEADER_SIZE  3
  92 
  93 // Maximum IP data bytes in an ABSI/IMP packet.  2036 = MAX_PKT_BYTES(2048) -
  94 // the 12-byte IMP leader, matching src/tcpip's absi_io_.pl1 Max_bits=16384
  95 // (see NET_MAX_TALLY below) -- raised 2026-08 from the original 1008 bytes
  96 // (from the OLD system_library_network stack's internet_absi.pl1: "2 data
  97 // bit (8064) unaligned" -> 8064/8 = 1008), which was too small for a
  98 // real-internet, MTU-sized IP packet arriving over the new src/tcpip stack's
  99 // default-gateway path and caused DCW corruption (see NET_MAX_TALLY).
 100 # define GATEWAY_MAX_DATA  2036
 101 
 102 // Frame header size for gateway communication (2-byte big-endian length prefix)
 103 # define NET_FRAME_HEADER_SIZE  2
 104 
 105 // Maximum 8-bit packet size (12-byte IMP leader + GATEWAY_MAX_DATA bytes of IP data)
 106 # define MAX_PKT_BYTES  (12 + GATEWAY_MAX_DATA)
 107 
 108 /* Maximum plausible DDCW_TALLY for any NET channel buffer, in 36-bit words.
 109  * The read channel uses buffer_size=456 (absi_io_.pl1: divide(16384+35,36)=456)
 110  * and the write channel uses a variable tally of 1-455 depending on packet size.
 111  * The IOM workspace is WS_SIZE=1024 words (one IOM page) -- 456 leaves 568
 112  * words of margin under that true hardware ceiling.
 113  * TALLY=0 (IOM convention for 4096) and TALLY > NET_MAX_TALLY both indicate
 114  * DCW corruption; see the validation checks in net_cmd cases 001 and 011.
 115  * Raised 2026-08 from 228/256 in lockstep with absi_io_.pl1's Max_bits
 116  * (8160 -> 16384) -- keep these in sync; a mismatch here silently corrupts
 117  * DCW state (oversized reads/writes overflowing the buffer[NET_MAX_TALLY]
 118  * arrays below) instead of failing cleanly.  */
 119 # define NET_MAX_TALLY  456
 120 
 121 /* Backoff after a PTW failure: do not reconnect for this many seconds.
 122  * When Multics's memory manager pages out the Internet daemon's IOM DMA buffer
 123  * pages after overnight idle, the PTW check fails.  Without a backoff the
 124  * terminate-interrupt -> net_cmd(001) -> net_connect cycle repeats every
 125  * ~60 ms (too fast for the memory manager to page the data back in).
 126  * The backoff check lives in net_connect() so it catches ALL reconnect
 127  * paths, including net_send_packet() (Multics WRITE cmd) which previously
 128  * bypassed the per-event check and caused ~91 ms reconnect cycles.     */
 129 # define PTW_BACKOFF_SECS  15
 130 
 131 /* Timeout before declaring the channel "masked+stuck" and forcing recovery.
 132  * See masked_since in net_dev_state and the masked-channel check in
 133  * net_process_event for details.                                            */
 134 # define MASKED_STUCK_TIMEOUT_SECS  30
 135 
 136 /* Timeout before releasing an IOM channel that has been waiting for the NET
 137  * to connect.  Keeps the channel from staying in IOM_CMD_PENDING indefinitely
 138  * when multics_ip_gateway is not running.                                   */
 139 # define NET_ABSENT_TIMEOUT_SECS    30
 140 
 141 /* Minimum valid DDCW_ADDR for the NET read channel workspace.
 142  *
 143  * The read channel workspace (absi_io_.pl1 "rws") layout:
 144  *  dcl  1 rws aligned based (db.read.wsp),
 145  *        2 statq (0:db.read.n_buffers - 1) like istat,   -- offset 0
 146  *        2 rss_idcw like idcw,                           -- offset n_buffers*8
 147  *        2 list (0:db.read.n_buffers - 1),               -- offset n_buffers*8+1
 148  *          3 idcw like idcw,
 149  *          3 dcw like dcw,
 150  *        2 tdcw like tdcw,                               -- offset n_buffers*8+1+n_buffers*2
 151  *        2 buffer (0:db.read.n_buffers - 1),
 152  *          3 error bit (1),
 153  *          3 n_bits fixed bin (24),                      -- packed with error into 1 word
 154  *          3 data bit (36 * db.read.buffer_size);        -- buffer[0].data starts here
 155  *
 156  * With n_buffers=4 (divide(1022, 229+2+8)=4) and size(istat)=8 words:
 157  *   statq:     offsets  0-31 (4 x 8 = 32 words)
 158  *   rss_idcw:  offset  32    (1 word)
 159  *   list:      offsets 33-40 (4 x 2 = 8 words)
 160  *   tdcw:      offset  41    (1 word)
 161  *   buffer[0].error+n_bits: offset 42 (1 word, packed together)
 162  *   buffer[0].data:         offset 43 <- first valid DDCW_ADDR
 163  *
 164  * Any DDCW_ADDR < 43 is invalid (it points into the control structures,
 165  * not into a data buffer).  DDCW_ADDR=0 in particular is set by
 166  * iom_list_service when it processes the TDCW at tdcw.address=0 (the TDCW
 167  * wrap case), storing the TDCW's DATA_ADDRESS field (=0) into p->DDCW_ADDR.  */
 168 # define NET_FIRST_BUFFER_OFFSET    43
 169 
 170 static void net_init_dev_state(void);
 171 
 172 # if defined(TESTING)
 173 static void dumppkt(char *hdr, word36 *buf, uint words);
 174 # endif
 175 
 176 struct net_dev_state
 177 {
 178   int gateway_socket;          /* connected socket to gateway process        */
 179   u_char want_to_read;         /* flag: Multics has a pending read           */
 180   uint read_unit_idx;          /* saved IOM unit index for pending read      */
 181   uint read_unit_chan;         /* saved IOM channel for pending read         */
 182   u_char delivery_succeeded;   /* set by net_cmd(READ) when IOM accepts pkt */
 183   time_t want_to_read_since;   /* wall-clock time when want_to_read was set  */
 184   time_t ptw_failed_at;        /* wall-clock time of last PTW check failure  */
 185   time_t masked_since;         /* wall-clock time channel first seen masked while connected */
 186 } net_dev_state;
 187 
 188 static struct net_state
 189   {
 190     char device_name[MAX_DEV_NAME_LEN];
 191   } net_state[N_NET_UNITS_MAX];
 192 
 193 # define N_NET_UNITS  2 // default
 194 
 195 # define UNIT_FLAGS \
 196         ( UNIT_FIX | UNIT_ATTABLE | UNIT_ROABLE | UNIT_DISABLE | UNIT_IDLE )
 197 
 198 UNIT net_unit[N_NET_UNITS_MAX] = {
 199   {
 200     UDATA(NULL, UNIT_FLAGS, 0),
 201     0,   0,   0,   0,   0,
 202     NULL,  NULL,  NULL,  NULL
 203   }
 204 };
 205 
 206 # define NET_UNIT_IDX(uptr)  (( uptr ) - net_unit )
 207 
 208 static DEBTAB net_dt[] = {
 209      { "NOTIFY", DBG_NOTIFY, NULL },
 210      { "INFO",   DBG_INFO,   NULL },
 211      { "ERR",    DBG_ERR,    NULL },
 212      { "WARN",   DBG_WARN,   NULL },
 213      { "DEBUG",  DBG_DEBUG,  NULL },
 214      { "ALL",    DBG_ALL,    NULL }, // Don't move as it messes up DBG message
 215      { NULL,     0,          NULL }
 216 };
 217 
 218 static t_stat
 219 net_show_nunits(UNUSED FILE *st, UNUSED UNIT *uptr, UNUSED int val,
     /* [previous][next][first][last][top][bottom][index][help] */
 220                 UNUSED const void *desc)
 221 {
 222   sim_printf("Number of NET units in system is %d\r\n", net_dev.numunits);
 223 
 224   return SCPE_OK;
 225 }
 226 
 227 static t_stat
 228 net_set_nunits(UNUSED UNIT *uptr, UNUSED int32 value, const char *cptr,
     /* [previous][next][first][last][top][bottom][index][help] */
 229                UNUSED void *desc)
 230 {
 231   if (!cptr)
 232     {
 233       return SCPE_ARG;
 234     }
 235 
 236   int n = atoi(cptr);
 237   if (n < 1 || n > N_NET_UNITS_MAX)
 238     {
 239       return SCPE_ARG;
 240     }
 241 
 242   net_dev.numunits = (uint32)n;
 243 
 244   return SCPE_OK;
 245 }
 246 
 247 static t_stat
 248 net_show_device_name(UNUSED FILE *st, UNIT *uptr, UNUSED int val,
     /* [previous][next][first][last][top][bottom][index][help] */
 249                      UNUSED const void *desc)
 250 {
 251   int n = (int)NET_UNIT_IDX(uptr);
 252 
 253   if (n < 0 || n >= N_NET_UNITS_MAX)
 254     {
 255       return SCPE_ARG;
 256     }
 257 
 258   if (net_state[n].device_name[1] != 0)
 259     {
 260       sim_printf("name     : %s", net_state[n].device_name);
 261     }
 262   else
 263     {
 264       sim_printf("name     : NET%d", n);
 265     }
 266 
 267   return SCPE_OK;
 268 }
 269 
 270 static t_stat
 271 net_set_device_name(UNIT *uptr, UNUSED int32 value, const char *cptr,
     /* [previous][next][first][last][top][bottom][index][help] */
 272                     UNUSED void *desc)
 273 {
 274   int n = (int)NET_UNIT_IDX(uptr);
 275 
 276   if (n < 0 || n >= N_NET_UNITS_MAX)
 277     {
 278       return SCPE_ARG;
 279     }
 280 
 281   if (cptr)
 282     {
 283       strncpy(net_state[n].device_name, cptr, MAX_DEV_NAME_LEN - 1);
 284       net_state[n].device_name[MAX_DEV_NAME_LEN - 1] = 0;
 285     }
 286   else
 287     {
 288       net_state[n].device_name[0] = 0;
 289     }
 290 
 291   return SCPE_OK;
 292 }
 293 
 294 static t_stat
 295 net_show_socket_path(UNUSED FILE *st, UNUSED UNIT *uptr, UNUSED int val,
     /* [previous][next][first][last][top][bottom][index][help] */
 296                      UNUSED const void *desc)
 297 {
 298   sim_printf("NET socket path: %s\r\n", gateway_socket_path);
 299   return SCPE_OK;
 300 }
 301 
 302 static t_stat
 303 net_set_socket_path(UNUSED UNIT *uptr, UNUSED int32 value, const char *cptr,
     /* [previous][next][first][last][top][bottom][index][help] */
 304                     UNUSED void *desc)
 305 {
 306   if (!cptr || strlen(cptr) == 0)
 307     return SCPE_ARG;
 308   if (strlen(cptr) >= GATEWAY_SOCKET_PATH_MAX - 1)
 309     {
 310       sim_printf("Gateway socket path too long (max %d chars)\r\n",
 311                  GATEWAY_SOCKET_PATH_MAX - 1);
 312       return SCPE_ARG;
 313     }
 314   strncpy(gateway_socket_path, cptr, GATEWAY_SOCKET_PATH_MAX - 1);
 315   gateway_socket_path[GATEWAY_SOCKET_PATH_MAX - 1] = '\0';
 316   sim_printf("NET gateway socket path set to: %s\r\n", gateway_socket_path);
 317   return SCPE_OK;
 318 }
 319 
 320 # define UNIT_WATCH  UNIT_V_UF
 321 
 322 static MTAB net_mod[] = {
 323 # if !defined(SPEED)
 324   { UNIT_WATCH, 1, "WATCH",   "WATCH",   0, 0, NULL, NULL },
 325   { UNIT_WATCH, 0, "NOWATCH", "NOWATCH", 0, 0, NULL, NULL },
 326 # endif /* if !defined(SPEED) */
 327   {
 328     MTAB_XTD | MTAB_VDV | MTAB_NMO | MTAB_VALR, /* Mask               */
 329     0,                                          /* Match              */
 330     "NUNITS",                                   /* Print string       */
 331     "NUNITS",                                   /* Match string       */
 332     net_set_nunits,                             /* Validation routine */
 333     net_show_nunits,                            /* Display routine    */
 334     "Number of NET units in the system",        /* Value descriptor   */
 335     NULL                                        /* Help               */
 336   },
 337   {
 338     MTAB_XTD | MTAB_VUN | MTAB_VALR | MTAB_NC,  /* Mask               */
 339     0,                                          /* Match              */
 340     "NAME",                                     /* Print string       */
 341     "NAME",                                     /* Match string       */
 342     net_set_device_name,                        /* Validation routine */
 343     net_show_device_name,                       /* Display routine    */
 344     "Set the device name",                      /* Value descriptor   */
 345     NULL                                        /* Help               */
 346   },
 347   {
 348     MTAB_XTD | MTAB_VDV | MTAB_VALR | MTAB_NC,  /* Mask               */
 349     0,                                          /* Match              */
 350     "PATH",                                     /* Print string       */
 351     "PATH",                                     /* Match string       */
 352     net_set_socket_path,                        /* Validation routine */
 353     net_show_socket_path,                       /* Display routine    */
 354     "Unix domain socket path for gateway connection", /* Value descriptor */
 355     NULL                                        /* Help               */
 356   },
 357   MTAB_eol
 358 };
 359 
 360 static t_stat
 361 net_reset(UNUSED DEVICE *dptr)
     /* [previous][next][first][last][top][bottom][index][help] */
 362 {
 363   return SCPE_OK;
 364 }
 365 
 366 static t_stat
 367 netAttach(UNIT *uptr, const char *cptr)
     /* [previous][next][first][last][top][bottom][index][help] */
 368 {
 369   if (!cptr)
 370     {
 371       return SCPE_ARG;
 372     }
 373 
 374   // If we're already attached, then detach ...
 375   if (( uptr->flags & UNIT_ATT ) != 0)
 376     {
 377       detach_unit(uptr);
 378     }
 379 
 380   uptr->flags |= UNIT_ATT;
 381 
 382   return SCPE_OK;
 383 }
 384 
 385 // Detach (connect) ...
 386 static t_stat
 387 netDetach(UNIT *uptr)
     /* [previous][next][first][last][top][bottom][index][help] */
 388 {
 389   if (( uptr->flags & UNIT_ATT ) == 0)
 390     {
 391       return SCPE_OK;
 392     }
 393 
 394   uptr->flags &= ~(unsigned int)UNIT_ATT;
 395 
 396   return SCPE_OK;
 397 }
 398 
 399 DEVICE net_dev = {
 400   "NET",       /* Name                */
 401   net_unit,    /* Units               */
 402   NULL,        /* Registers           */
 403   net_mod,     /* Modifiers           */
 404   N_NET_UNITS, /* #units              */
 405   10,          /* Address radix       */
 406   24,          /* Address width       */
 407   1,           /* Address increment   */
 408   8,           /* Data radix          */
 409   36,          /* Data width          */
 410   NULL,        /* Examine             */
 411   NULL,        /* Deposit             */
 412   net_reset,   /* Reset               */
 413   NULL,        /* Boot                */
 414   netAttach,   /* Attach              */
 415   netDetach,   /* Detach              */
 416   NULL,        /* Context             */
 417   DEV_DEBUG,   /* Flags               */
 418   0,           /* Debug control flags */
 419   net_dt,      /* Debug flag names    */
 420   NULL,        /* Memory size change  */
 421   NULL,        /* Logical name        */
 422   NULL,        /* Help                */
 423   NULL,        /* Attach help         */
 424   NULL,        /* Attach context      */
 425   NULL,        /* Description         */
 426   NULL         /* End                 */
 427 };
 428 
 429 /*
 430  * net_init()
 431  */
 432 
 433 // Once-only initialization
 434 
 435 void
 436 net_init(void)
     /* [previous][next][first][last][top][bottom][index][help] */
 437 {
 438   (void)memset(net_state, 0, sizeof ( net_state ));
 439   net_init_dev_state();
 440 }
 441 
 442 static void
 443 net_init_dev_state(void)
     /* [previous][next][first][last][top][bottom][index][help] */
 444 {
 445   (void)memset(&net_dev_state, 0, sizeof ( net_dev_state ));
 446   net_dev_state.gateway_socket = -1;
 447 }
 448 
 449 /*
 450  * Connect to the gateway process via Unix domain socket.
 451  * Returns 0 on success, -1 on failure.
 452  */
 453 static int
 454 net_connect(void)
     /* [previous][next][first][last][top][bottom][index][help] */
 455 {
 456   if (net_dev_state.gateway_socket >= 0)
 457     {
 458       return 0; /* already connected */
 459     }
 460 
 461   /* Refuse to reconnect while in PTW-failure backoff.
 462    * This check must live here (not only in net_process_event) because
 463    * net_send_packet() and net_recv_packet() both call us directly and
 464    * would otherwise bypass the backoff, causing ~91 ms reconnect cycles
 465    * even while the 15-second timer is counting down.                  */
 466   if (net_dev_state.ptw_failed_at > 0)
 467     {
 468       if (time(NULL) - net_dev_state.ptw_failed_at < PTW_BACKOFF_SECS)
 469         return -1; /* still in backoff - give memory manager time to work */
 470       net_dev_state.ptw_failed_at = 0; /* backoff expired */
 471     }
 472 
 473   int sock = socket(AF_UNIX, SOCK_STREAM, 0);
 474   if (sock < 0)
 475     {
 476       (void)sir_error("%s:%d socket(AF_UNIX) error: %s (%d)",
 477                       __func__, __LINE__, xstrerror_l(errno), errno);
 478       return -1;
 479     }
 480 
 481   struct sockaddr_un addr = {0};
 482   addr.sun_family = AF_UNIX;
 483   snprintf(addr.sun_path, sizeof(addr.sun_path), "%.*s",
 484            (int)(sizeof(addr.sun_path) - 1), gateway_socket_path);
 485 
 486   if (connect(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0)
 487     {
 488       int s_errno = errno;
 489       size_t path_len = strlen(gateway_socket_path);
 490       if (path_len > sizeof(addr.sun_path) - 1) //-V547
 491         {
 492           (void)sir_notice("%s:%d: unix socket address was %zu characters; use a PATH at most %zu characters long.",
 493                            __func__, __LINE__, path_len, sizeof(addr.sun_path) - 1);
 494         }
 495       (void)sir_notice("%s: connect(%s) error: %s (%d)",
 496                        __func__, addr.sun_path, xstrerror_l(s_errno), s_errno);
 497       if (s_errno != EBADF)
 498         {
 499           close(sock);
 500         }
 501       return -1;
 502     }
 503 
 504   /* Set non-blocking for polling */
 505   int err = dps8_uv__nonblock(sock, 1);
 506   if (err < 0)
 507     {
 508       (void)sir_error ("dps8_uv__nonblock: set non-blocking failed: %s:%d: %s (%s)",
 509                        __func__, __LINE__, uv_strerror(err), uv_err_name(err));
 510       if (err != UV_EBADF)
 511         {
 512           close(sock);
 513         }
 514       return -1;
 515     }
 516 
 517   net_dev_state.gateway_socket = sock;
 518   (void)sir_info("%s:%d: connected to IP gateway at %s (fd %d)",
 519                  __func__, __LINE__, gateway_socket_path, sock);
 520   return 0;
 521 }
 522 
 523 /*
 524  * Send a length-prefixed 8-bit packet to the gateway process.
 525  * The packet has already been converted from 36-bit words to 8-bit bytes.
 526  * Returns 0 on success, -1 on failure.
 527  */
 528 static int
 529 net_send_packet(u_char *pkt8, int pktlen)
     /* [previous][next][first][last][top][bottom][index][help] */
 530 {
 531   if (net_dev_state.gateway_socket < 0)
 532     {
 533       if (net_connect() < 0)
 534         {
 535           return -1;
 536         }
 537     }
 538 
 539   /* Temporarily set blocking for the send */
 540   int err = dps8_uv__nonblock(net_dev_state.gateway_socket, 0);
 541   if (err < 0)
 542     {
 543       (void)sir_error("dps8_uv__nonblock: set blocking failed: %s:%d: %s (%s)",
 544                       __func__, __LINE__, uv_strerror(err), uv_err_name(err));
 545       if (err != UV_EBADF)
 546         {
 547           close(net_dev_state.gateway_socket);
 548         }
 549       net_dev_state.gateway_socket = -1;
 550       return -1;
 551     }
 552 
 553   u_char hdr[NET_FRAME_HEADER_SIZE];
 554   hdr[0] = (pktlen >> 8) & 0xFF;
 555   hdr[1] = pktlen & 0xFF;
 556 
 557   int rc = 0;
 558   ssize_t w = write(net_dev_state.gateway_socket, hdr, NET_FRAME_HEADER_SIZE);
 559   if (w != NET_FRAME_HEADER_SIZE)
 560     {
 561       int s_errno = errno;
 562       (void)sir_error("%s%d: write header failed: %s (%d)",
 563                       __func__, __LINE__, xstrerror_l(s_errno), s_errno);
 564       if (s_errno != EBADF)
 565         {
 566           close(net_dev_state.gateway_socket);
 567         }
 568       net_dev_state.gateway_socket = -1;
 569       rc = -1;
 570     }
 571   else
 572     {
 573       w = write(net_dev_state.gateway_socket, pkt8, pktlen);
 574       if (w != pktlen)
 575         {
 576           int s_errno = errno;
 577           (void)sir_error("%s:%d: write body failed: wrote %zd of %d: %s (%d)",
 578                           __func__, __LINE__, w, pktlen, xstrerror_l(s_errno), s_errno);
 579           if (s_errno != EBADF)
 580             {
 581               close(net_dev_state.gateway_socket);
 582             }
 583           net_dev_state.gateway_socket = -1;
 584           rc = -1;
 585         }
 586     }
 587 
 588   /* Restore non-blocking */
 589   if (net_dev_state.gateway_socket >= 0)
 590     {
 591         int err = dps8_uv__nonblock(net_dev_state.gateway_socket, 1);
 592         if (err < 0)
 593           {
 594             (void)sir_error ("dps8_uv__nonblock: set non-blocking failed: %s:%d: %s (%s)",
 595                              __func__, __LINE__, uv_strerror(err), uv_err_name(err));
 596             if (err != UV_EBADF)
 597               {
 598                 close(net_dev_state.gateway_socket);
 599               }
 600             return -1;
 601           }
 602     }
 603 
 604 return rc;
 605 }
 606 
 607 /*
 608  * Try to receive a length-prefixed 8-bit packet from the gateway process
 609  * (non-blocking). Returns the number of bytes received (>0), 0 if nothing
 610  * available, or -1 on error.
 611  */
 612 static int
 613 net_recv_packet(u_char *pkt8, int maxlen)
     /* [previous][next][first][last][top][bottom][index][help] */
 614 {
 615   if (net_dev_state.gateway_socket < 0)
 616     {
 617       if (net_connect() < 0)
 618         {
 619           return 0; /* not connected, nothing to read */
 620         }
 621     }
 622 
 623   /* Non-blocking read of the 2-byte length header */
 624   u_char hdr[NET_FRAME_HEADER_SIZE];
 625   ssize_t r = recv(net_dev_state.gateway_socket, hdr, NET_FRAME_HEADER_SIZE, MSG_PEEK);
 626 
 627   if (r == 0)
 628     {
 629       /* Connection closed */
 630       int s_errno = errno;
 631       (void)sir_info("%s:%d: gateway connection closed", __func__, __LINE__);
 632       if (s_errno != EBADF)
 633         {
 634           close(net_dev_state.gateway_socket);
 635         }
 636       net_dev_state.gateway_socket = -1;
 637       return -1;
 638     }
 639 
 640   if (r < 0)
 641     {
 642       int s_errno = errno;
 643       if (s_errno == EAGAIN || s_errno == EWOULDBLOCK)
 644         {
 645           return 0; /* nothing available */
 646         }
 647 
 648       (void)sir_error("%s:%d: recv error: %s (%d)",
 649                       __func__, __LINE__, xstrerror_l(s_errno), s_errno);
 650       if (s_errno != EBADF)
 651         {
 652           close(net_dev_state.gateway_socket);
 653         }
 654       net_dev_state.gateway_socket = -1;
 655       return -1;
 656     }
 657 
 658   if (r < NET_FRAME_HEADER_SIZE)
 659     {
 660       return 0; /* incomplete header, wait for more */
 661     }
 662 
 663   /* We have the full header, switch to blocking to read the rest */
 664   int err = dps8_uv__nonblock(net_dev_state.gateway_socket, 0);
 665   if (err < 0)
 666     {
 667       (void)sir_error("dps8_uv__nonblock: set blocking failed: %s:%d: %s (%s)",
 668                       __func__, __LINE__, uv_strerror(err), uv_err_name(err));
 669       if (err != UV_EBADF)
 670         {
 671           close(net_dev_state.gateway_socket);
 672         }
 673       net_dev_state.gateway_socket = -1;
 674       return -1;
 675     }
 676 
 677   /* Actually consume the header */
 678   r = read(net_dev_state.gateway_socket, hdr, NET_FRAME_HEADER_SIZE);
 679   if (r != NET_FRAME_HEADER_SIZE)
 680     {
 681       int s_errno = errno;
 682       (void)sir_error("%s:%d: read header failed", __func__, __LINE__);
 683       if (s_errno != EBADF)
 684         {
 685           close(net_dev_state.gateway_socket);
 686         }
 687       net_dev_state.gateway_socket = -1;
 688       return -1;
 689     }
 690 
 691   int pktlen = (hdr[0] << 8) | hdr[1];
 692   if (pktlen <= 0 || pktlen > maxlen)
 693     {
 694       int s_errno = errno;
 695       (void)sir_error("%s:%d bad packet length %d", __func__, __LINE__, pktlen);
 696       if (s_errno != EBADF)
 697         {
 698           close(net_dev_state.gateway_socket);
 699         }
 700       net_dev_state.gateway_socket = -1;
 701       return -1;
 702     }
 703 
 704   /* Read the packet body - loop until all pktlen bytes are received.
 705    * A single read() may return fewer bytes than requested on TCP when the
 706    * sender's writes span multiple segments.  Treating a short read as fatal
 707    * (as the old code did) caused spurious ECONNRESET during FTP sessions. */
 708   {
 709     int nread = 0;
 710     while (nread < pktlen)
 711       {
 712         r = read(net_dev_state.gateway_socket, pkt8 + nread, pktlen - nread);
 713         if (r <= 0)
 714           {
 715             int s_errno = errno;
 716             (void)sir_error("%s:%d: read body failed at offset %d of %d: %s (%d)",
 717                             __func__, __LINE__, nread, pktlen,
 718                             r == 0 ? "EOF" : xstrerror_l(errno), errno);
 719             if (s_errno != EBADF)
 720               {
 721                 close(net_dev_state.gateway_socket);
 722               }
 723             net_dev_state.gateway_socket = -1;
 724             return -1;
 725           }
 726         nread += r;
 727       }
 728     r = nread;
 729   }
 730 
 731   /* Restore non-blocking */
 732   err = dps8_uv__nonblock(net_dev_state.gateway_socket, 1);
 733   if (err < 0)
 734     {
 735       (void)sir_error ("dps8_uv__nonblock: set non-blocking failed: %s:%d: %s (%s)",
 736                        __func__, __LINE__, uv_strerror(err), uv_err_name(err));
 737       if (err != UV_EBADF)
 738         {
 739           close(net_dev_state.gateway_socket);
 740         }
 741       net_dev_state.gateway_socket = -1;
 742       return -1;
 743     }
 744 
 745   return (int)r;
 746 }
 747 
 748 /*
 749  * Convert an 8-bit ABSI packet to 36-bit IOM words using binary-mode packing.
 750  *
 751  * The ABSI/IMP hardware sends a raw serial bit stream to the IOM.  The IOM
 752  * packs it in "binary mode": bits are stored continuously, 36 per word, with
 753  * no per-byte overhead.  Byte j starts at stream bit j*8, which falls in
 754  * word j*8/36 at bit position j*8%36.  Most bytes fit entirely in one word;
 755  * every 9th byte (at bit_in_word=32) spans a word boundary as 4+4 bits.
 756  *
 757  * The imp_leader structure in internet_absi.pl1 is declared "unaligned" and
 758  * is overlaid directly on this binary-mode bit stream.  Byte 0 (pad1+format)
 759  * must occupy Multics bits 0-7 of word 0, so format (bits 4-7) reads as 0xF.
 760  * Using 9-bit byte encoding would shift the byte to bits 1-8, making the
 761  * format check in internet_absi fail with "Bad IMP leader".
 762  */
 763 static void
 764 pkt8_to_word36(u_char *pkt8, int pktlen, word36 *buf, uint maxwords)
     /* [previous][next][first][last][top][bottom][index][help] */
 765 {
 766   uint j;
 767   (void)memset(buf, 0, maxwords * sizeof(word36));
 768 
 769   for (j = 0; j < (uint)pktlen; j++)
 770     {
 771       uint stream_bit  = j * 8u;
 772       uint word_idx    = stream_bit / 36u;
 773       uint bit_in_word = stream_bit % 36u;
 774 
 775       if (word_idx >= maxwords)
 776         break;
 777 
 778       if (bit_in_word <= 28u)
 779         {
 780           /* Entire byte fits in one word */
 781           putbits36_8(&buf[word_idx], bit_in_word, pkt8[j]);
 782         }
 783       else
 784         {
 785           /* bit_in_word == 32: byte spans two words (4 bits each) */
 786           putbits36_4(&buf[word_idx], 32u, pkt8[j] >> 4);
 787           if (word_idx + 1u < maxwords)
 788             putbits36_4(&buf[word_idx + 1u], 0u, pkt8[j] & 0x0Fu);
 789         }
 790     }
 791 }
 792 
 793 /*
 794  * Convert 36-bit IOM words to an 8-bit ABSI packet using binary-mode unpacking.
 795  * Returns the total packet length in bytes.
 796  */
 797 static int
 798 word36_to_pkt8(word36 *buf, uint words, u_char *pkt8, int maxlen)
     /* [previous][next][first][last][top][bottom][index][help] */
 799 {
 800   /* Binary mode: words*36 bits total; each byte takes 8 bits */
 801   int total = (int)((uint)words * 36u / 8u);
 802   if (total > maxlen)
 803     total = maxlen;
 804 
 805   int j;
 806   for (j = 0; j < total; j++)
 807     {
 808       uint stream_bit  = (uint)j * 8u;
 809       uint word_idx    = stream_bit / 36u;
 810       uint bit_in_word = stream_bit % 36u;
 811 
 812       if (word_idx >= words)
 813         break;
 814 
 815       if (bit_in_word <= 28u)
 816         {
 817           pkt8[j] = getbits36_8(buf[word_idx], bit_in_word);
 818         }
 819       else
 820         {
 821           /* bit_in_word == 32: byte spans two words (4 bits each) */
 822           u_char hi = (u_char)getbits36_4(buf[word_idx], 32u);
 823           u_char lo = (word_idx + 1u < words)
 824                     ? (u_char)getbits36_4(buf[word_idx + 1u], 0u)
 825                     : 0u;
 826           pkt8[j] = (hi << 4) | lo;
 827         }
 828     }
 829 
 830   /* Trim to the actual packet length using the IMP 1822L leader's
 831    * message_length field (bytes 10-11, big-endian, in BITS).
 832    * Total packet = 12-byte IMP leader + message_length/8 bytes of IP data.
 833    * (internet_absi.pl1: in_len = divide(in_pkt.message_length, 8, 16, 0)) */
 834   if (total >= 12)
 835     {
 836       int msg_len_bits = ((pkt8[10] & 0xFF) << 8) | (pkt8[11] & 0xFF);
 837       int real_len = 12 + (msg_len_bits / 8);
 838       if (real_len < total)
 839         total = real_len;
 840     }
 841 
 842   return total;
 843 }
 844 
 845 static iom_cmd_rc_t
 846 get_ddcw(iom_chan_data_t *p, uint iom_unit_idx, uint chan, bool *ptro,
     /* [previous][next][first][last][top][bottom][index][help] */
 847          uint expected_tally, uint *tally)
 848 {
 849 # if defined(TESTING)
 850   cpu_state_t * cpup = _cpup;
 851 # endif
 852   bool  send, uff;
 853   int   rc = iom_list_service(iom_unit_idx, chan, ptro, &send, &uff);
 854 
 855   if (rc < 0)
 856     {
 857       p->stati = 05001;
 858       (void)sir_warn("%s:%d list service failed", __func__, __LINE__);
 859 
 860       return IOM_CMD_ERROR;
 861     }
 862 
 863   if (uff)
 864     {
 865       (void)sir_warn("%s:%d ignoring uff", __func__, __LINE__);
 866     }
 867 
 868   if (!send)
 869     {
 870       (void)sir_warn("%s%d nothing to send", __func__, __LINE__);
 871       p->stati = 05001;
 872 
 873       return IOM_CMD_ERROR;
 874     }
 875 
 876   if (IS_IDCW(p) || IS_TDCW(p))
 877     {
 878       (void)sir_warn("%s:%d expected DDCW", __func__, __LINE__);
 879       p->stati = 05001;
 880 
 881       return IOM_CMD_ERROR;
 882     }
 883 
 884   *tally = p->DDCW_TALLY;
 885 
 886   if (*tally == 0)
 887     {
 888       sim_debug(DBG_DEBUG, &net_dev,
 889                 "%s: Tally of zero interpreted as 010000(4096)\r\n", __func__);
 890       *tally = 4096;
 891     }
 892 
 893   sim_debug(DBG_DEBUG, &net_dev,
 894             "%s: Tally %d (%o)\r\n", __func__, *tally, *tally);
 895 
 896   if (expected_tally && *tally != expected_tally)
 897     {
 898       (void)sir_warn("net_dev call expected tally of %d; got %d",
 899                      expected_tally, *tally);
 900       p->stati = 05001;
 901 
 902       return IOM_CMD_ERROR;
 903     }
 904 
 905   return IOM_CMD_PROCEED;
 906 }
 907 
 908 static char *
 909 cmd_name(int code)
     /* [previous][next][first][last][top][bottom][index][help] */
 910 {
 911   switch (code)
 912     {
 913     case 000:
 914       return "Request status";
 915 
 916     case 001:
 917       return "Read";
 918 
 919     case 011:
 920       return "Write";
 921 
 922     case 020:
 923       return "Host switch down";
 924 
 925     case 040:
 926       return "Reset status";
 927 
 928     case 042:
 929       return "Disable Bus Back";
 930 
 931     case 043:
 932       return "Enable Bus Back";
 933 
 934     case 060:
 935       return "Host switch up";
 936 
 937     default:
 938       return "Unknown";
 939     }
 940 }
 941 
 942 # if defined(TESTING)
 943 /*
 944  * dumppkt: Debug dump of a 36-bit ABSI (IMP 1822L) packet.
 945  * The first 12 bytes (3 words) are the IMP 1822L leader; the remainder
 946  * is the IP payload.
 947  *
 948  * IMP 1822L leader layout (96 bits, internet_absi.pl1):
 949  *   byte  0: pad1(4b) | format(4b)       - format must be 0xF for data
 950  *   byte  1: source_network(8b)
 951  *   byte  2: pad2(4b) | trace(1b) | flags(3b)
 952  *   byte  3: message_type(8b)            - 0=regular, 4=NOP, 5=RFNM, 10=reset
 953  *   byte  4: handling_type(8b)
 954  *   byte  5: host(8b)
 955  *   byte  6: port_exp(8b)
 956  *   byte  7: imp(8b)
 957  *   bytes 8-9: message_id(12b) | subtype(4b)
 958  *   bytes 10-11: message_length(16b)     - length of IP payload in BITS
 959  */
 960 static void
 961 dumppkt(char *hdr, word36 *buf, uint words)
     /* [previous][next][first][last][top][bottom][index][help] */
 962 {
 963   int i;
 964   if (words < GATEWAY_PACKET_HEADER_SIZE)
 965     {
 966       (void)sir_notice("%s: packet too small (%d words, need %d for IMP leader)",
 967                        hdr, words, GATEWAY_PACKET_HEADER_SIZE);
 968       return;
 969     }
 970 
 971   /* Extract 12 bytes of IMP leader from the first 3 words (binary mode) */
 972   u_char leader[12];
 973   for (i = 0; i < 12; i++)
 974     {
 975       uint stream_bit  = (uint)i * 8u;
 976       uint word_idx    = stream_bit / 36u;
 977       uint bit_in_word = stream_bit % 36u;
 978       if (bit_in_word <= 28u)
 979         leader[i] = getbits36_8(buf[word_idx], bit_in_word);
 980       else
 981         {
 982           u_char hi = (u_char)getbits36_4(buf[word_idx], 32u);
 983           u_char lo = (u_char)getbits36_4(buf[word_idx + 1u], 0u);
 984           leader[i] = (hi << 4) | lo;
 985         }
 986     }
 987 
 988   int format       = leader[0] & 0x0F;
 989   int src_net      = leader[1];
 990   int trace        = (leader[2] >> 3) & 1;
 991   int imp_flags    = leader[2] & 0x07;
 992   int msg_type     = leader[3];
 993   int handling     = leader[4];
 994   int host         = leader[5];
 995   int port_exp     = leader[6];
 996   int imp          = leader[7];
 997   int msg_id       = ((leader[8] & 0xFF) << 4) | ((leader[9] >> 4) & 0x0F);
 998   int subtype      = leader[9] & 0x0F;
 999   int msg_len_bits = ((leader[10] & 0xFF) << 8) | (leader[11] & 0xFF);
1000   int ip_bytes     = msg_len_bits / 8;
1001 
1002   (void)sir_notice("%s packet (%d words)", hdr, words);
1003   (void)sir_notice("IMP leader: format=%d net=%d trace=%d flags=%d",
1004                    format, src_net, trace, imp_flags);
1005   (void)sir_notice("  type=%d handling=%d host=%d port_exp=%d imp=%d",
1006                    msg_type, handling, host, port_exp, imp);
1007   (void)sir_notice("  msg_id=0x%03x subtype=%d msg_len=%d bits (%d bytes IP)",
1008                    msg_id, subtype, msg_len_bits, ip_bytes);
1009 
1010   int pklen = GATEWAY_PACKET_HEADER_SIZE + (ip_bytes / 4)
1011             + (ip_bytes % 4 ? 1 : 0);
1012   if (pklen > (int)words)
1013     {
1014       pklen = (int)words;
1015     }
1016 
1017   for (i = 0; i < pklen; i++)
1018     {
1019       int lh = getbits36_18(buf[i],  0);
1020       int rh = getbits36_18(buf[i], 18);
1021       int b0 = getbits36_9 (buf[i],  0);
1022       int b1 = getbits36_9 (buf[i],  9);
1023       int b2 = getbits36_9 (buf[i], 18);
1024       int b3 = getbits36_9 (buf[i], 27);
1025       (void)sir_notice(" %d: %06o,,%06o = 0x%02x %02x %02x %02x",
1026                        i, lh, rh, b0, b1, b2, b3);
1027     }
1028 
1029   (void)sir_notice("EOP");
1030 }
1031 # endif
1032 
1033 /* Forward declarations - defined later in this file */
1034 static int net_check_dma_ptw(uint iom_unit_idx, uint chan, uint max_words);
1035 static void net_validate_dcw_state(uint iom_unit_idx, uint chan, int expected_tally,
1036                                    const char *caller);
1037 
1038 /*
1039  * net_validate_dcw_state() - Diagnostic: validate DCW list state vs Multics memory.
1040  *
1041  * Called when an anomalous DDCW_TALLY or DDCW_ADDR is detected.  Logs:
1042  *   1. Current iom_chan_data fields (cached values the IOM is using).
1043  *   2. The raw DCW list entries from Multics memory (workspace page 0),
1044  *      so we can see whether the corruption is in the cache or in memory.
1045  *
1046  * The read channel workspace structure (from absi_io_.pl1 "rws"):
1047  *   offset  0-31: statq[0..3]   - 4 x istat (8 words each = 32 words)
1048  *   offset    32: rss_idcw      - 1 word
1049  *   offset 33-40: list[0..3]    - 4 x (idcw + dcw) = 8 words
1050  *   offset    41: tdcw          - 1 word (transfer/wrap-around DCW)
1051  *   offset    42: buffer[0] error+n_bits - 1 word (packed)
1052  *   offset 43+:  buffer[0] data - 456 words per buffer slot
1053  *
1054  * CORRECTNESS NOTE (2026-08, unverified): this "4 buffer slots" layout
1055  * (statq[0..3], list[0..3]) does not match db.read.n_buffers as actually
1056  * computed by absi_io_.pl1 (WS_SIZE-2)/(buffer_size+3+size(istat)), which
1057  * comes out to roughly 17 for buffer_size=228 and roughly 8 for the new
1058  * buffer_size=456 -- not 4.  This offset map is likely stale/approximate;
1059  * treat it (and "the workspace fits in one IOM page" below) as unverified
1060  * documentation, not a relied-upon fact, until someone checks it against
1061  * absi_io_.pl1's actual "rws"/"wws" structure layout.  This function is
1062  * diagnostic-only (a warning dump), so a wrong offset map here doesn't
1063  * itself cause corruption -- it would just mislabel the dumped fields.
1064  *
1065  * Expected DDCW_ADDR values should be within [0, 07777] octal (the
1066  * workspace spans WS_SIZE=4096 words = 4 IOM pages, not 1 -- see note
1067  * above).
1068  * Expected DDCW_TALLY is 456 (read channel) or 1-455 (write channel,
1069  * variable: 1 + divide(nbits, 36) where nbits = packet_bytes * 8).
1070  *
1071  * If DDCW_ADDR or DDCW_TALLY is outside those bounds, DCW corruption has
1072  * occurred - most likely from a prior PTW-failure DMA write to address ~= 0
1073  * that overwrote the IOM mailbox and caused iom_list_service to follow a
1074  * bad LPW pointer into arbitrary memory.
1075  */
1076 static void
1077 net_validate_dcw_state(uint iom_unit_idx, uint chan, int expected_tally,
     /* [previous][next][first][last][top][bottom][index][help] */
1078                         const char *caller)
1079   {
1080     iom_chan_data_t *p = &iom_chan_data[iom_unit_idx][chan];
1081 
1082     /* 1. Dump the cached iom_chan_data state */
1083     (void)sir_warn("DCW_VALIDATE [%s] chan=%d:\r\n"
1084                    "  cached: DDCW_ADDR=0%o DDCW_TALLY=%d (expected ~%d)"
1085                    "  DDCW_22_23_TYPE=%d",
1086                    caller, chan,  p->DDCW_ADDR, (int)p->DDCW_TALLY, expected_tally, (int)p->DDCW_22_23_TYPE);
1087     (void)sir_warn("  LPW_DCW_PTR=0%o LPW_TALLY=%d", p->LPW_DCW_PTR, (int)p->LPW_TALLY);
1088     (void)sir_warn("  PCW_PAGE_TABLE_PTR=0%o PCW_63_PTP=%d PCW_64_PGE=%d SEG=%d",
1089                    p->PCW_PAGE_TABLE_PTR, (int)p->PCW_63_PTP, (int)p->PCW_64_PGE, (int)p->SEG);
1090     (void)sir_warn("  in_use=%d masked=%d", (int)p->in_use, (int)p->masked);
1091 
1092     /* 2. Sanity-check DDCW_ADDR range (workspace = WS_SIZE=4096 words = 4 IOM
1093      * pages -- see the correctness note above net_validate_dcw_state's own
1094      * header comment; this bound was previously 01777 (1 page), which is
1095      * demonstrably too tight given the real db.read.n_buffers computation. */
1096     if (p->DDCW_ADDR > 07777)
1097       {
1098         (void)sir_warn("DCW_VALIDATE: DDCW_ADDR=0%o is OUTSIDE workspace range"
1099                       " [0, 07777] - LPW likely corrupted", p->DDCW_ADDR);
1100       }
1101 
1102     /* 3. Read DCW list from Multics memory in paged mode */
1103     if (!p->PCW_63_PTP || !p->PCW_64_PGE)
1104       {
1105         (void)sir_warn("DCW_VALIDATE: not in paged mode"
1106                        " (PTP=%d PGE=%d) - skipping memory read",
1107                        (int)p->PCW_63_PTP, (int)p->PCW_64_PGE);
1108         return;
1109       }
1110 
1111     /* Look up the workspace page 0 PTW */
1112     word24 pgte0 = (((word24)(p->PCW_PAGE_TABLE_PTR & MASK18)) << 6)
1113                  + (((word24)(p->SEG & 1)) << 8)
1114                  + 0u; /* page 0 */
1115 
1116     word36 ptw0 = 0;
1117     iom_core_read(iom_unit_idx, pgte0, &ptw0, __func__);
1118 
1119     int ptw0_valid = ((ptw0 & 0740000777747llu) == 04llu);
1120     (void)sir_warn("DCW_VALIDATE: workspace page 0 PTW at pgte=0%o:"
1121                    " 0%012llo (%s)",
1122                    pgte0, (unsigned long long)ptw0,
1123                    ptw0_valid ? "valid" : "INVALID");
1124 
1125     if (!ptw0_valid)
1126       {
1127         (void)sir_warn("DCW_VALIDATE: page 0 PTW invalid -"
1128                        " cannot read DCW list from Multics memory");
1129         return;
1130       }
1131 
1132     /* Physical base address of workspace (bits 4-17 of PTW, shifted left 10) */
1133     word24 phys_base = ((word24)((ptw0 >> 18) & MASK14)) << 10;
1134     (void)sir_warn("DCW_VALIDATE: workspace physical base = 0%o", phys_base);
1135 
1136     /* 4. Read and validate each of the 4 IDCW+DDCW pairs.
1137      * The list array (rws.list) occupies offsets 33-40 in the workspace
1138      * (after statq[0..3]=32 words and rss_idcw=1 word):
1139      *   slot i: IDCW at offset 33 + 2*i, DDCW at offset 33 + 2*i+1  */
1140     (void)sir_warn("DCW_VALIDATE: list entries from memory"
1141                    " (expected TALLY=%d, ADDR in [0,07777]):", expected_tally);
1142     for (int i = 0; i < 4; i++)
1143       {
1144         word24 idcw_phys = phys_base + (word24)(33 + 2 * i);
1145         word24 ddcw_phys = phys_base + (word24)(33 + 2 * i + 1);
1146 
1147         word36 idcw_word = 0, ddcw_word = 0;
1148         iom_core_read(iom_unit_idx, idcw_phys, &idcw_word, __func__);
1149         iom_core_read(iom_unit_idx, ddcw_phys, &ddcw_word, __func__);
1150 
1151         /* DDCW layout: bits 0-17 = address, bits 24-35 = tally */
1152         uint ddcw_addr  = (uint)((ddcw_word >> 18) & MASK18);
1153         uint ddcw_tally = (uint)(ddcw_word & 0xFFFu);
1154 
1155         int anomalous = (ddcw_tally == 0
1156                       || (int)ddcw_tally > (expected_tally + 2)
1157                       || ddcw_addr > 07777u);
1158 
1159         (void)sir_warn("  slot[%d]: IDCW=0%012llo DDCW=0%012llo"
1160                        " addr=0%o tally=%d%s",
1161                        i,
1162                        (unsigned long long)idcw_word,
1163                        (unsigned long long)ddcw_word,
1164                        ddcw_addr, ddcw_tally,
1165                        anomalous ? " *** ANOMALOUS" : "");
1166       }
1167 
1168     /* 5. Read the word at LPW_DCW_PTR to see what iom_list_service last fetched */
1169     word18 lpw_ptr = p->LPW_DCW_PTR;
1170     if (lpw_ptr <= 07777u)
1171       {
1172         word36 lpw_word = 0;
1173         iom_core_read(iom_unit_idx, phys_base + (word24)lpw_ptr, &lpw_word, __func__);
1174         (void)sir_warn("DCW_VALIDATE: mem[LPW_DCW_PTR=0%o] = 0%012llo",
1175                        lpw_ptr, (unsigned long long)lpw_word);
1176       }
1177     else
1178       {
1179         (void)sir_warn("DCW_VALIDATE: LPW_DCW_PTR=0%o is OUTSIDE workspace"
1180                        " [0, 07777] - LPW pointer corrupted", lpw_ptr);
1181       }
1182   }
1183 
1184 static iom_cmd_rc_t
1185 net_cmd(uint iom_unit_idx, uint chan)
     /* [previous][next][first][last][top][bottom][index][help] */
1186 {
1187 # if defined(TESTING)
1188   cpu_state_t * cpup = _cpup;
1189 # endif
1190   iom_chan_data_t *p = &iom_chan_data[iom_unit_idx][chan];
1191 
1192   sim_debug(DBG_TRACE, &net_dev,
1193             "net_cmd CHAN_CMD %o DEV_CODE %o DEV_CMD %o COUNT %o\r\n",
1194             p->IDCW_CHAN_CMD, p->IDCW_DEV_CODE, p->IDCW_DEV_CMD, p->IDCW_COUNT);
1195 
1196   // Not IDCW?
1197   if (IS_NOT_IDCW(p))
1198     {
1199       (void)sir_warn("%s:%d Unexpected IOTx", __func__, __LINE__);
1200 
1201       return IOM_CMD_ERROR;
1202     }
1203 
1204   bool ptro;
1205 
1206   sim_debug(DBG_DEBUG, &net_dev, "net_cmd %#o (%s)\r\n",
1207             p->IDCW_DEV_CMD, cmd_name(p->IDCW_DEV_CMD));
1208 
1209   switch (p->IDCW_DEV_CMD)
1210     {
1211     case 000: // CMD 00 Request status
1212     {
1213       p->stati = 04000;
1214       sim_debug(DBG_DEBUG, &net_dev, "net request status\r\n");
1215     }
1216     break;
1217 
1218     case 001: // CMD 01 Read
1219     {
1220 # if defined(DPS8_NET_DIAG)
1221       /* Diagnostic instrumentation: count every entry into this case -- i.e.
1222        * every time Multics announces it is ready to receive the next packet
1223        * (sets want_to_read=1 further down). Paired with the delivery-side
1224        * counter in net_process_event, this shows whether want_to_read ever
1225        * gets set at all during a stall, and whether it's set but delivery
1226        * never happens (the silent early return at the top of
1227        * net_process_event when want_to_read is already false would
1228        * otherwise be invisible). Rate-limited to a periodic summary --
1229        * this fires on ordinary background traffic (NOPs, SSDP, keepalives),
1230        * not just the connection under test, so logging every entry floods
1231        * the console.                                                       */
1232       {
1233         static unsigned long read_cmd_entries = 0;
1234         static time_t last_read_cmd_log = 0;
1235         read_cmd_entries++;
1236         time_t now_rc = time(NULL);
1237         if (now_rc - last_read_cmd_log >= 2)
1238           {
1239             (void)sir_warn("%s:%d CMD 001 entries so far=%lu (last chan %d)\r\n",
1240                            __func__, __LINE__, read_cmd_entries, chan);
1241             fflush(stdout); /* stdout is fully-buffered when piped through
1242                                 the launch script's `| tee`, not line-buffered
1243                                 as it would be on a bare terminal -- without
1244                                 this, sparse diagnostic output like this can
1245                                 sit unflushed indefinitely. */
1246             last_read_cmd_log = now_rc;
1247           }
1248       }
1249 # endif
1250       sim_debug(DBG_DEBUG, &net_dev, "%s: net_dev_$read\r\n", __func__);
1251 
1252       const uint    expected_tally = 0;
1253       uint          tally;
1254       iom_cmd_rc_t  rc
1255         = get_ddcw(p, iom_unit_idx, chan, &ptro, expected_tally, &tally);
1256       if (rc)
1257         {
1258           return rc;
1259         }
1260 
1261       /* Validate the DMA PTW before any IOM buffer access.
1262        * If Multics's memory manager has paged out the IOM buffer (e.g. after
1263        * extended idle), iom_indirect_data_service() would compute physical
1264        * address ~= 0 from the zero PTW and either read garbage or corrupt the
1265        * IOM mailbox area (addresses 0-0777).  Return IOM_CMD_DISCONNECT so the
1266        * IOM sends a terminate interrupt; Multics can then re-establish the
1267        * channel with properly pinned memory.
1268        *
1269        * max_words=NET_MAX_TALLY: packets are at most NET_MAX_TALLY 36-bit
1270        * words, which fits within the first IOM page (1024 words).
1271        * Using max_words=0 (full DDCW_TALLY=4096 words = 4 pages) would cause
1272        * false positives when Multics's memory manager pages out unused pages
1273        * 1-3 of the large DCW buffer overnight - those pages are never touched
1274        * by the DMA since the actual payload is at most NET_MAX_TALLY words. */
1275       if (!net_check_dma_ptw(iom_unit_idx, chan, NET_MAX_TALLY))
1276         {
1277           (void)sir_warn("%s:%d: CMD 001 invalid DMA PTW on chan %d - "
1278                          "skipping buffer access, sending terminate interrupt",
1279                          __func__, __LINE__, chan);
1280           net_dev_state.ptw_failed_at = time(NULL);
1281           p->stati = 04000;
1282           return IOM_CMD_DISCONNECT;
1283         }
1284 
1285       /* Sanity-check DDCW_TALLY.
1286        *
1287        * Legitimate ABSI read DDCWs have TALLY set by absi_io_.pl1:
1288        * buffer_size = divide(16384+35, 36) = 456 words.  TALLY=0 (IOM convention
1289        * for 4096) or an implausibly large value means the DCW entry was
1290        * corrupted.  The primary corruption path (TDCW wrap -> DDCW_ADDR=0 ->
1291        * IDS overwrites DCW list) is now blocked by the DDCW_ADDR range check
1292        * in net_process_event; this check is retained as defense-in-depth.  */
1293       if (p->DDCW_TALLY == 0 || p->DDCW_TALLY > NET_MAX_TALLY)
1294         {
1295           (void)sir_warn("%s:%d: CMD 001 implausible DDCW_TALLY=%d on chan %d"
1296                          " (expected %d, max %d - DCW corruption?)"
1297                          " sending terminate interrupt",
1298                          __func__, __LINE__, p->DDCW_TALLY, chan, NET_MAX_TALLY, NET_MAX_TALLY);
1299           net_validate_dcw_state(iom_unit_idx, chan, NET_MAX_TALLY, "CMD001-TALLY");
1300           p->stati = 04000;
1301           return IOM_CMD_DISCONNECT;
1302         }
1303 
1304       /* Read current buffer and complete DDCW processing */
1305       word36  buffer[NET_MAX_TALLY];
1306       uint    words_processed;
1307       iom_indirect_data_service(
1308         iom_unit_idx, chan, buffer, &words_processed, false);
1309 
1310       sim_debug(DBG_DEBUG, &net_dev,
1311                 "%s: Read unit %#x chan %#x (%d), %d words\r\n",
1312                 __func__, iom_unit_idx, chan, chan, words_processed);
1313 
1314       /*
1315        * Mark that Multics wants to read. The actual data delivery happens
1316        * in net_process_event() when the gateway sends us a packet.
1317        */
1318       net_dev_state.want_to_read           = 1;
1319       net_dev_state.want_to_read_since     = time(NULL);
1320       net_dev_state.read_unit_idx          = iom_unit_idx;
1321       net_dev_state.read_unit_chan         = chan;
1322 
1323       /* Write back to IOM to complete the DDCW processing.
1324        * This is required before returning IOM_CMD_PENDING -
1325        * without it the IOM channel state is inconsistent and
1326        * Multics will timeout and mask the channel.
1327        * (cf. new_dps8m_chaos_code/dps8_net.c.new lines 525-526)
1328        */
1329       iom_indirect_data_service(
1330         iom_unit_idx, chan, buffer, &words_processed, true);
1331 
1332       p->stati = 04000;
1333       /* Signal net_process_event that this delivery attempt succeeded
1334        * (iom_continue_channel successfully called net_cmd). */
1335       net_dev_state.delivery_succeeded = 1;
1336       return IOM_CMD_PENDING;
1337     }
1338     /*NOTREACHED*/ /* unreachable */
1339     break;
1340 
1341     case 011: // CMD 11 Write
1342     {
1343 # if defined(DPS8_NET_DIAG)
1344       /* Diagnostic instrumentation: unconditional (warning, not debug)
1345        * count of every entry into this case, logged before any validation
1346        * check has a chance to bail out early. Compare this count against
1347        * how many "ARPAnet write" lines Multics's own PL/1 trace logs for
1348        * the same test -- if they match, the byte loss is happening inside
1349        * a syscall this code believes succeeded (nothing left to find here);
1350        * if this count is lower, some DCW never reaches this handler at all,
1351        * which is a genuine IOM-dispatch bug upstream of net_cmd.            */
1352       {
1353         static unsigned long write_cmd_entries = 0;
1354         static time_t last_write_cmd_log = 0;
1355         write_cmd_entries++;
1356         time_t now_wc = time(NULL);
1357         if (now_wc - last_write_cmd_log >= 2)
1358           {
1359             (void)sir_warn("%s:%d CMD 011 entries so far=%lu (last chan %d)\r\n",
1360                            __func__, __LINE__, write_cmd_entries, chan);
1361             fflush(stdout);
1362             last_write_cmd_log = now_wc;
1363           }
1364       }
1365 # endif
1366       sim_debug(DBG_DEBUG, &net_dev, "%s: net_dev_$write\r\n", __func__);
1367 
1368       const uint    expected_tally = 0;
1369       uint          tally;
1370       iom_cmd_rc_t  rc
1371         = get_ddcw(p, iom_unit_idx, chan, &ptro, expected_tally, &tally);
1372 
1373       /* Check get_ddcw result (same as CMD 001 - missing this check was a
1374        * bug: a failed get_ddcw left p->DDCW_ADDR stale, causing the PTW
1375        * check below to validate the wrong page).
1376        *
1377        * NOTE: get_ddcw() returns IOM_CMD_PROCEED (0) on success or
1378        * IOM_CMD_ERROR (-1) on failure - never IOM_CMD_PENDING.
1379        * Returning IOM_CMD_ERROR here sends a TERMINATE interrupt (not marker)
1380        * via the rc<0 path in doPayloadChannel/iom_continue_channel.  Multics
1381        * sees the terminate and re-issues the write; the packet content is
1382        * lost (silent frame drop -> "Unordered frame" in gateway log).
1383        * Logged as WARNING so we can correlate with gateway-side drops.        */
1384       if (rc)
1385         {
1386           (void)sir_warn("%s:%d: CMD 011 get_ddcw failed rc=%d on chan %d"
1387                          " - packet will be silently dropped (frame loss)",
1388                          __func__, __LINE__, rc, chan);
1389           return rc;
1390         }
1391 
1392       /* Validate PTW before DMA access (prevents mailbox corruption if
1393        * Multics has paged out the WRITE channel buffer).
1394        * max_words=NET_MAX_TALLY: same rationale as CMD 001 - the write
1395        * payload is at most NET_MAX_TALLY-1 36-bit words, which fits within
1396        * the current IOM page.  Checking the full DDCW_TALLY (4096 words = 4
1397        * pages) would cause false positives when pages beyond the payload
1398        * are paged out, leading to tight IOM_CMD_DISCONNECT loops.         */
1399       if (!net_check_dma_ptw(iom_unit_idx, chan, NET_MAX_TALLY))
1400         {
1401           (void)sir_warn("%s:%d: CMD 011 invalid DMA PTW on chan %d - "
1402                          "skipping buffer access, sending terminate interrupt",
1403                          __func__, __LINE__, chan);
1404           net_dev_state.ptw_failed_at = time(NULL);
1405           close(net_dev_state.gateway_socket);
1406           net_dev_state.gateway_socket = -1; /* discard queued NOOPs */
1407           p->stati = 04000;
1408           return IOM_CMD_DISCONNECT;
1409         }
1410 
1411       /* Sanity-check DDCW_TALLY (same rationale as CMD 001).
1412        * The write channel uses variable TALLY (absi_io_.pl1:
1413        * nwords = 1 + divide(nbits, 36), range 1-455 for packets up to
1414        * GATEWAY_MAX_DATA bytes).
1415        * iom_indirect_data_service's READ path (first call below, reading
1416        * the outgoing packet from Multics memory) ignores cnt and walks
1417        * all p->DDCW_TALLY words; TALLY=0 -> 4096-word walk -> same console-
1418        * flooding cascade as CMD 001.                                    */
1419       if (p->DDCW_TALLY == 0 || p->DDCW_TALLY > NET_MAX_TALLY)
1420         {
1421           (void)sir_warn("%s:%d: CMD 011 implausible DDCW_TALLY=%d on chan %d"
1422                          " (expected 1-%d, max %d - DCW corruption?)"
1423                          " sending terminate interrupt",
1424                          __func__, __LINE__, p->DDCW_TALLY, chan, NET_MAX_TALLY - 1, NET_MAX_TALLY);
1425           net_validate_dcw_state(iom_unit_idx, chan, NET_MAX_TALLY - 1, "CMD011-TALLY");
1426           net_dev_state.ptw_failed_at = time(NULL);
1427           close(net_dev_state.gateway_socket);
1428           net_dev_state.gateway_socket = -1;
1429           p->stati = 04000;
1430           return IOM_CMD_DISCONNECT;
1431         }
1432 
1433       word36  buffer[NET_MAX_TALLY];
1434       uint    words_processed;
1435       iom_indirect_data_service(
1436         iom_unit_idx, chan, buffer, &words_processed, false);
1437 
1438       /* Diagnostic: iom_indirect_data_service can silently stop short of
1439        * p->DDCW_TALLY (e.g. a page-boundary/PTW issue mid-transfer) without
1440        * signaling an error -- word36_to_pkt8 below only sees words_processed,
1441        * so a short walk here silently truncates the outgoing packet at the
1442        * source, before it ever reaches net_send_packet's own (correct)
1443        * byte-count checks. Flag any mismatch loudly to catch this directly. */
1444       if (words_processed != p->DDCW_TALLY)
1445         {
1446           (void)sir_warn("%s:%d: CMD 011 short DMA read: words_processed=%u but "
1447                          "DDCW_TALLY=%d on chan %d -- packet truncated at source\r\n",
1448                          __func__, __LINE__, words_processed, (int)p->DDCW_TALLY, chan);
1449           fflush(stdout);
1450         }
1451 
1452       sim_debug(DBG_DEBUG, &net_dev,
1453                 "%s: Write unit %#x chan %#x (%d), %d words\r\n",
1454                 __func__, iom_unit_idx, chan, chan, words_processed);
1455 # if defined(TESTING)
1456       if (sim_deb && (net_dev.dctrl & DBG_DEBUG))
1457         {
1458           dumppkt("Write", buffer, words_processed);
1459         }
1460 # endif
1461       /* Convert 36-bit words to 8-bit bytes */
1462       u_char pkt8[MAX_PKT_BYTES];
1463       int pktlen = word36_to_pkt8(buffer, words_processed, pkt8, MAX_PKT_BYTES);
1464 
1465       /* Send to gateway */
1466       int v = net_send_packet(pkt8, pktlen);
1467       if (v < 0)
1468         {
1469           /* Surface the failure to Multics instead of falling through to
1470            * the unconditional success status below -- previously a failed
1471            * send was logged only to the simulator's own console (never
1472            * visible to Multics) while Multics was told the write completed
1473            * normally. That left absi_io_.pl1's istat.er retry path unable
1474            * to ever fire, and higher-level code (e.g. a bare TCP ACK, which
1475            * PL/1's TCP layer never re-queues for retransmission) treating
1476            * data as sent when it never reached the gateway at all.        */
1477           (void)sir_warn("%s:%d net_send_packet failed", __func__, __LINE__);
1478           p->stati = 05001;
1479           return IOM_CMD_ERROR;
1480         }
1481 
1482       /* Return value depends on IDCW control field:
1483        *
1484        * absi_io_ chains packets via the IDCW control field.  When a previous
1485        * packet's IDCW has control "10"b (CHAN_CTRL_PROCEED), the IOM continues
1486        * to the next IDCW without sending a terminate interrupt.  The last IDCW
1487        * in the chain has control "00"b (CHAN_CTRL_TERMINATE), which triggers a
1488        * terminate interrupt after that packet is processed.
1489        * doPayloadChannel's do-while loop processes each IDCW:
1490        *
1491        *   - IOM_CMD_PROCEED (0): loop continues -> iom_list_service advances
1492        *     to the next IDCW -> net_cmd(011) called again for the next packet.
1493        *   - IOM_CMD_DISCONNECT (2): sets terminate=true -> loop exits after
1494        *     this iteration -> terminate interrupt sent.
1495        *
1496        * Without this check, we always returned IOM_CMD_DISCONNECT for every
1497        * IDCW, causing the loop to exit after the FIRST packet in a chain.
1498        * Subsequent chained packets would be silently dropped.                   */
1499       rc = (p->IDCW_CHAN_CTRL == CHAN_CTRL_TERMINATE)
1500              ? IOM_CMD_DISCONNECT   /* terminate IDCW: send terminate interrupt */
1501              : IOM_CMD_PROCEED;     /* no-terminate IDCW: continue DCW list loop */
1502       p->stati  = 04000;
1503 
1504       /* Write-back: re-validates PTW before writing back to the Multics
1505        * write DMA buffer.  net_connect() inside net_send_packet() can
1506        * block briefly (Unix connect syscall), during which CPU A (Multics)
1507        * may page out the DMA buffer.  If the page is now gone, skip the
1508        * write-back (avoids fetch_IDSPTW warnings and address-0 corruption),
1509        * close the socket to flush queued NOOPs, and let the terminate
1510        * interrupt trigger Multics channel recovery.
1511        * max_words=NET_MAX_TALLY: same rationale as initial PTW check above -
1512        * check only the payload area, not unused pages beyond it.          */
1513       if (!net_check_dma_ptw(iom_unit_idx, chan, NET_MAX_TALLY))
1514         {
1515           (void)sir_warn("%s:%d: CMD 011 PTW invalid after net_send_packet on chan %d"
1516                          " - skipping write-back\r\n", __func__, __LINE__, chan);
1517           net_dev_state.ptw_failed_at = time(NULL);
1518           close(net_dev_state.gateway_socket);
1519           net_dev_state.gateway_socket = -1;
1520           return rc; /* IOM_CMD_DISCONNECT - sends terminate interrupt */
1521         }
1522 
1523       iom_indirect_data_service(
1524         iom_unit_idx, chan, buffer, &words_processed, true);
1525 
1526       return rc;
1527     }
1528     /*NOTREACHED*/ /* unreachable */
1529     break;
1530 
1531     case 006: // CMD 06 (seen during channel restart; acknowledge gracefully)
1532     {
1533       p->stati = 04000;
1534       sim_debug(DBG_DEBUG, &net_dev, "net cmd 006 (handled)\r\n");
1535     }
1536     break;
1537 
1538     case 020: // CMD 20 Host switch down
1539     {
1540       p->stati = 04000;
1541       sim_debug(DBG_DEBUG, &net_dev, "net host switch down\r\n");
1542 
1543       /* Symmetric counterpart to the CMD 060 "Host switch up" forwarding
1544        * above: internet_absi.pl1's `stop` issues this (via
1545        * iox_$control(iocbp, "host_down", ...)) when Internet.Daemon
1546        * detaches the NETR/NETW devices (logout, shutdown, or restart).
1547        * Purely informational for the gateway - no reinit is needed here,
1548        * since the next Host-switch-up sentinel already handles that - but
1549        * forwarding it lets the gateway log Multics-side attach/detach
1550        * events instead of only ever seeing "connected". */
1551       {
1552         u_char host_switch_down_sentinel = 0xA6;
1553         if (net_send_packet(&host_switch_down_sentinel, 1) < 0)
1554           {
1555             sim_debug(DBG_DEBUG, &net_dev,
1556                       "%s: failed to notify gateway of host switch down\r\n", __func__);
1557           }
1558       }
1559     }
1560     break;
1561 
1562     case 040: // CMD 40 Reset status
1563     {
1564       p->stati = 04000;
1565     }
1566     break;
1567 
1568     case 042: // CMD 42 Disable Bus Back
1569     {
1570       p->stati = 04000;
1571       sim_debug(DBG_DEBUG, &net_dev, "net disable bus back\r\n");
1572     }
1573     break;
1574 
1575     case 043: // CMD 43 Enable Bus Back
1576     {
1577       p->stati = 04000;
1578       sim_debug(DBG_DEBUG, &net_dev, "net enable bus back\r\n");
1579     }
1580     break;
1581 
1582     case 060: // CMD 60 Host switch up
1583     {
1584       p->stati = 04000;
1585       sim_debug(DBG_DEBUG, &net_dev, "net host switch up\r\n");
1586 
1587       /* internet_absi.pl1's reset_imp issues this (via
1588        * iox_$control(iocbp, "host_up", ...)) exactly once at the start of
1589        * every internet_absi run, i.e. every Internet.Daemon login/restart.
1590        * The gateway's Unix-domain-socket connection to us never closes
1591        * across a Multics-side daemon restart, so without forwarding this
1592        * there is no way for the gateway to know internet_absi dropped back
1593        * to STATE_DOWN and needs the NOP+Interface-Reset init sequence
1594        * resent.  Forward it as a 1-byte sentinel frame (HOST_SWITCH_UP_SENTINEL
1595        * in the gateway's imp.rs) using the same length-prefixed framing as
1596        * ordinary IMP data - a real IMP frame is always at least 12 bytes, so
1597        * this can never collide with legitimate Multics traffic.  Best-effort:
1598        * a failure here just means the gateway won't auto-reinit for this
1599        * particular restart, no different from before this existed. */
1600       {
1601         u_char host_switch_up_sentinel = 0xA5;
1602         if (net_send_packet(&host_switch_up_sentinel, 1) < 0)
1603           {
1604             sim_debug(DBG_DEBUG, &net_dev,
1605                       "%s: failed to notify gateway of host switch up\r\n", __func__);
1606           }
1607       }
1608     }
1609     break;
1610 
1611     default:
1612     {
1613       if (p->IDCW_DEV_CMD != 051) // ignore bootload console probe
1614         {
1615           (void)sir_warn("%s:%d: NET unrecognized device command  %02o",
1616                          __func__, __LINE__, p->IDCW_DEV_CMD);
1617         }
1618 
1619       p->stati       = 04501; // cmd reject, invalid opcode
1620       p->chanStatus  = chanStatIncorrectDCW;
1621     }
1622       return IOM_CMD_ERROR;
1623     }
1624 
1625   if (p->IDCW_CHAN_CMD == 0)
1626     {
1627       return IOM_CMD_DISCONNECT; // don't do DCW list
1628     }
1629 
1630   return IOM_CMD_PROCEED;
1631 }
1632 
1633 iom_cmd_rc_t
1634 net_iom_cmd(uint iom_unit_idx, uint chan)
     /* [previous][next][first][last][top][bottom][index][help] */
1635 {
1636   iom_chan_data_t *p = &iom_chan_data[iom_unit_idx][chan];
1637 
1638   // Is it an IDCW?
1639   if (IS_IDCW(p))
1640     {
1641       return net_cmd(iom_unit_idx, chan);
1642     }
1643 
1644   (void)sir_notice("%s%d: expected IDCW", __func__, __LINE__);
1645 
1646   return IOM_CMD_ERROR;
1647 }
1648 
1649 /*
1650  * net_check_dma_ptw() - Validate that ALL pages of the IOM DMA buffer for
1651  * channel `chan` have valid page table words (PTWs) before calling
1652  * iom_indirect_data_service().
1653  *
1654  * ioi_$workspace pins all workspace pages (DCW list and data buffers) in
1655  * physical memory for the duration of active I/O (while in_use=true).
1656  * Page eviction cannot occur.  However, if the DCW list becomes corrupted
1657  * (e.g., circular-buffer wrap during an RCP force-detach/reattach cycle),
1658  * DDCW_ADDR may point outside the wired workspace, where no PTW exists.
1659  * Calling iom_indirect_data_service() with PTW=0 computes physical address
1660  * ~= 0, overwriting the IOM mailbox area (addresses 0-0777).  That corruption
1661  * cascades: Multics's IOM interrupt handler reads garbage vectors ->
1662  * fault/interrupt storm -> 100% CPU and system hang.
1663  *
1664  * The buffer spans from DDCW_ADDR through DDCW_ADDR+tally-1, potentially
1665  * crossing page boundaries.  Only checking the first page misses unmapped
1666  * pages later in the buffer (symptom: fetch_IDSPTW warnings at addr 0o02000+
1667  * after a force-detach/reattach cycle).  This function validates ALL pages
1668  * in the range so that any zero PTW is caught before the DMA starts.
1669  *
1670  * This function replicates the PTW lookup from fetch_IDSPTW /
1671  * build_IDSPTW_address (both static in dps8_iom.c) so dps8_net.c can
1672  * pre-check validity without touching the IOM code.
1673  *
1674  * Returns: 1 if all PTWs in the buffer range are valid (safe to proceed),
1675  *          0 if any PTW is zero or otherwise invalid (skip the DMA).
1676  */
1677 static int
1678 net_check_dma_ptw(uint iom_unit_idx, uint chan, uint max_words)
     /* [previous][next][first][last][top][bottom][index][help] */
1679   {
1680     iom_chan_data_t * p = & iom_chan_data[iom_unit_idx][chan];
1681 
1682     /* If the channel is not in paged mode, no PTW to validate. */
1683     if (!p->PCW_63_PTP || !p->PCW_64_PGE)
1684       return 1;
1685 
1686     /* Determine the range of IOM pages the buffer spans.
1687      * tally=0 is interpreted as 4096 by get_ddcw / iom_indirect_data_service.
1688      * page numbers are 8-bit (IOM page table has at most 256 entries).
1689      *
1690      * max_words: when non-zero, caps the effective tally used for page range
1691      * calculation.  Use this when the caller knows it will only write a small
1692      * payload (e.g. net_process_event delivers packets of at most ~128
1693      * 36-bit words) so that pages beyond the actual payload are not validated
1694      * unnecessarily.  Pass 0 to use the full DDCW_TALLY (or 4096).          */
1695     uint   raw_tally  = p->DDCW_TALLY ? p->DDCW_TALLY : 4096;
1696     uint   tally      = (max_words && max_words < raw_tally) ? max_words : raw_tally;
1697     word18 start_page = (p->DDCW_ADDR         >> 10) & MASK8;
1698     word18 end_page   = ((p->DDCW_ADDR + tally - 1)  >> 10) & MASK8;
1699 
1700     /* Replicate build_IDSPTW_address() from dps8_iom.c for each page:
1701      *   pgte = ((PCW_PAGE_TABLE_PTR & MASK18) << 6)
1702      *        + ((SEG & 1)             <<    8)
1703      *        + (pageNumber            & MASK8)                               */
1704     for (word18 page = start_page; page <= end_page; page++)
1705       {
1706         word24 pgte = (((word24)(p->PCW_PAGE_TABLE_PTR & MASK18)) << 6)
1707                     + (((word24)(p->SEG & 1))                     <<  8)
1708                     + (page                                       & MASK8);
1709 
1710         word36 ptw;
1711         iom_core_read(iom_unit_idx, pgte, &ptw, __func__);
1712 
1713         /* Valid PTW has specific bits set; zero PTW means page not present. */
1714         if ((ptw & 0740000777747llu) != 04llu)
1715           {
1716             (void)sir_warn ("%s:%d: chan %d DDCW_ADDR 0%o page %u/%u: invalid PTW"
1717                             " 0%012llo at pgte 0%o"
1718                             " (PCW_PAGE_TABLE_PTR=0%o SEG=%d tally=%u)\r\n",
1719                             __func__, __LINE__, chan, p->DDCW_ADDR,
1720                             (unsigned)(page - start_page + 1),
1721                             (unsigned)(end_page - start_page + 1),
1722                             (unsigned long long)ptw, pgte,
1723                             p->PCW_PAGE_TABLE_PTR, (int)p->SEG, tally);
1724             return 0;
1725           }
1726       }
1727     return 1;
1728   }
1729 
1730 /*
1731  * net_process_event() - Called periodically from the emulator's event loop.
1732  *
1733  * If Multics has a pending read (want_to_read), poll the gateway socket for
1734  * incoming data.  If data is available, read it, convert from 8-bit to
1735  * 36-bit words, write to the current IOM workspace buffer[N] via
1736  * iom_indirect_data_service, then call iom_continue_channel() to:
1737  *
1738  *   1. Advance DDCW_ADDR to workspace buffer[N+1] (via one loop iteration
1739  *      of doPayloadChannel: fetch IDCW[N+1] -> call net_cmd(read) ->
1740  *      get_ddcw() sets DDCW_ADDR = buffer[N+1]).
1741  *
1742  *   2. Send a marker interrupt so absi_io_'s process_read_status fires.
1743  *      The interrupt's stat.offset causes the runx counter to advance,
1744  *      triggering absi_io_'s read_record to consume buffer[N].
1745  *
1746  * Because the interrupt is a MARKER (not terminate), absi_io_'s connect()
1747  * sees running=true and does NOT call ioi_$connect.  The channel remains
1748  * pending with DDCW_ADDR pointing to buffer[N+1], ready for the next
1749  * incoming packet.  want_to_read stays set (net_cmd restores it inside
1750  * iom_continue_channel).
1751  *
1752  * TDCW wrap-around is handled transparently by iom_list_service inside
1753  * iom_continue_channel.
1754  */
1755 void
1756 net_process_event(void)
     /* [previous][next][first][last][top][bottom][index][help] */
1757 {
1758 # if defined(TESTING)
1759   cpu_state_t * cpup = _cpup;
1760 # endif
1761   if (!net_dev_state.want_to_read)
1762     {
1763       return;
1764     }
1765 
1766   uint iom_unit_idx = net_dev_state.read_unit_idx;
1767   uint chan          = net_dev_state.read_unit_chan;
1768   iom_chan_data_t * p = & iom_chan_data[iom_unit_idx][chan];
1769 
1770   /* If the channel has been masked (Multics sent a PCW with MSK=1), stop
1771    * trying to deliver packets until Multics re-enables it with a fresh
1772    * Connect PCW (MSK=0).  want_to_read is restored by net_cmd() when
1773    * Multics issues the new read command inside doPayloadChannel.
1774    *
1775    * NOTE: We do NOT check !in_use here.  The IOM sets in_use=false after
1776    * a terminate interrupt (e.g. DCW fault), but iom_continue_channel()
1777    * needs to run in that case so it can advance the DCW list and generate
1778    * the terminate interrupt that tells Multics to re-issue the read command.
1779    * Blocking on !in_use (without masked) prevents that signalling and
1780    * causes Multics to stall for ~30 seconds until its d102 timer fires. */
1781   {
1782     if (p->masked)
1783       {
1784         net_dev_state.want_to_read = 0;
1785 
1786         /* If the gateway socket is connected, track how long the channel has been
1787          * stuck masked.  After MASKED_STUCK_TIMEOUT_SECS, close the socket and
1788          * send a terminate interrupt.  This breaks the deadlock:
1789          *   - masked channel -> no delivery -> gateway inflight=30 -> no more NOOPs
1790          *   - ioi_masked$timer fires but "masked while in use" prevents recovery
1791          * The terminate interrupt tells Multics the I/O failed so it re-issues
1792          * READ with a fresh channel state.  The gateway-absent path then handles
1793          * reconnection cleanly.                                               */
1794         if (net_dev_state.gateway_socket >= 0)
1795           {
1796             time_t now = time(NULL);
1797             if (net_dev_state.masked_since == 0)
1798               {
1799                 net_dev_state.masked_since = now;
1800                 sim_debug(DBG_DEBUG, &net_dev,
1801                           "%s: channel %d masked while gateway connected; "
1802                           "starting stuck timer\r\n", __func__, chan);
1803               }
1804             else if (now - net_dev_state.masked_since >= MASKED_STUCK_TIMEOUT_SECS)
1805               {
1806                 (void)sir_warn("%s:%d: channel %d masked+stuck for %d+ seconds; "
1807                                "closing gateway socket and sending terminate interrupt "
1808                                "to force recovery\r\n",
1809                                __func__, __LINE__, chan, MASKED_STUCK_TIMEOUT_SECS);
1810                 close(net_dev_state.gateway_socket);
1811                 net_dev_state.gateway_socket = -1;
1812                 net_dev_state.masked_since = 0;
1813                 send_terminate_interrupt(net_dev_state.read_unit_idx,
1814                                          net_dev_state.read_unit_chan);
1815               }
1816           }
1817         else
1818           {
1819             /* gateway not connected.  Reset the masked-stuck timer (it's only
1820              * meaningful when the gateway is connected), but also run the
1821              * gateway-absent timeout so the IOM channel's in_use=true state is
1822              * eventually released.
1823              *
1824              * Without this: masked=true causes us to return here every 10ms,
1825              * bypassing the gateway-absent timeout check below.  If the channel
1826              * is masked AND the gateway is absent, in_use never clears, and
1827              * ioi_masked$timer fires every ~4 minutes finding "chan N masked
1828              * while in use" - a permanent stuck deadlock.
1829              *
1830              * With this: after NET_ABSENT_TIMEOUT_SECS (30 s) we send a
1831              * terminate interrupt.  in_use becomes false.  The next
1832              * ioi_masked$timer invocation successfully reconnects the masked
1833              * channel (no longer masked+in_use), and normal operation
1834              * resumes once the gateway connects again.                          */
1835             net_dev_state.masked_since = 0;
1836             time_t now_ma = time(NULL);
1837             if (net_dev_state.want_to_read_since > 0 &&
1838                 now_ma - net_dev_state.want_to_read_since >= NET_ABSENT_TIMEOUT_SECS)
1839               {
1840                 sim_debug(DBG_DEBUG, &net_dev,
1841                           "%s: gateway absent + channel %d masked for %d+ s; "
1842                           "sending terminate interrupt to release in_use\r\n",
1843                           __func__, chan, NET_ABSENT_TIMEOUT_SECS);
1844                 net_dev_state.want_to_read = 0;
1845                 send_terminate_interrupt(net_dev_state.read_unit_idx,
1846                                          net_dev_state.read_unit_chan);
1847               }
1848           }
1849         return;
1850       }
1851 
1852     /* Channel is not masked - clear the stuck timer */
1853     net_dev_state.masked_since = 0;
1854   }
1855 
1856   /* Periodic diagnostic: log socket state every 60 seconds at debug level */
1857   {
1858     static time_t last_diag = 0;
1859     time_t now = time(NULL);
1860     if (now - last_diag >= 60)
1861       {
1862         sim_debug(DBG_DEBUG, &net_dev,
1863                   "NET diag: want_to_read=%d gateway_socket=%d unit=%d chan=%d\r\n",
1864                   net_dev_state.want_to_read,
1865                   net_dev_state.gateway_socket,
1866                   net_dev_state.read_unit_idx,
1867                   net_dev_state.read_unit_chan);
1868         last_diag = now;
1869       }
1870   }
1871 
1872   /* If the gateway is not connected, try to connect first.  If still not
1873    * connected after NET_ABSENT_TIMEOUT_SECS, release the IOM channel via
1874    * a terminate interrupt.  Without this release, the channel stays in
1875    * IOM_CMD_PENDING indefinitely; Multics's ioi_masked$timer eventually
1876    * fires and sends a mask PCW while the channel is still "in use",
1877    * producing spurious "doConnectChan: chan N masked while in use" and
1878    * "ioi_masked$timer: Timeout on channel" console messages.
1879    * After the terminate interrupt, Multics re-issues the READ command and
1880    * want_to_read_since is reset, so the cycle repeats quietly every
1881    * NET_ABSENT_TIMEOUT_SECS seconds until the gateway connects. */
1882   if (net_dev_state.gateway_socket < 0)
1883     {
1884       /* net_connect() handles PTW backoff and rate-limiting internally. */
1885       net_connect();
1886       if (net_dev_state.gateway_socket < 0)
1887         {
1888           /* Still not connected.  Check whether we have been waiting too long. */
1889           time_t now2 = time(NULL);
1890           if (now2 - net_dev_state.want_to_read_since >= NET_ABSENT_TIMEOUT_SECS)
1891             {
1892               sim_debug(DBG_DEBUG, &net_dev,
1893                         "%s: gateway absent for %d+ seconds, releasing IOM channel %d "
1894                         "via terminate interrupt\r\n",
1895                         __func__, NET_ABSENT_TIMEOUT_SECS,
1896                         net_dev_state.read_unit_chan);
1897               net_dev_state.want_to_read = 0;
1898               send_terminate_interrupt(net_dev_state.read_unit_idx,
1899                                        net_dev_state.read_unit_chan);
1900             }
1901           return;
1902         }
1903       /* gateway just connected.  Reset the timestamp so we don't immediately
1904        * time out on the next invocation. */
1905       net_dev_state.want_to_read_since = time(NULL);
1906     }
1907 
1908   /* Try to receive a packet from the gateway */
1909   u_char pkt8[MAX_PKT_BYTES];
1910   int pktlen = net_recv_packet(pkt8, MAX_PKT_BYTES);
1911 
1912   if (pktlen <= 0)
1913     {
1914       return; /* nothing available or error */
1915     }
1916 
1917   sim_debug(DBG_DEBUG, &net_dev,
1918             "%s: received %d bytes from gateway for unit %d chan %d\r\n",
1919             __func__, pktlen, iom_unit_idx, chan);
1920 
1921   /* Guard: only deliver if the IOM channel has an active, pinned I/O
1922    * operation.  ioi_$workspace wires all workspace pages (DCW list, IDCWs,
1923    * DDCWs, and data buffers) while in_use=true.  When in_use=false the
1924    * channel has no active I/O: DDCW_ADDR and DDCW_TALLY may be stale or
1925    * corrupted (e.g., left over from the previous iom_continue_channel call),
1926    * and the workspace pin is not guaranteed.  Attempting delivery in this
1927    * state risks writing to a bad address.
1928    *
1929    * want_to_read=1 with in_use=false is the pathological state that produces
1930    * the overnight "fetch_IDSPTW: addr 07766 ptw 000000000000" cascade: net_cmd
1931    * was called via iom_continue_channel and stored a stale DDCW_ADDR, then
1932    * send_terminate_interrupt set in_use=false without clearing want_to_read.
1933    * The fix: if in_use is false, discard the packet, clear want_to_read, and
1934    * wait for Multics to re-issue ioi_$connect (which will set in_use=true and
1935    * establish a fresh, valid DDCW for us).                                   */
1936 
1937   if (! p->in_use)
1938     {
1939       (void)sir_warn("%s:%d: chan %d not in active I/O (in_use=false) but want_to_read=1"
1940                      " - discarding packet, clearing want_to_read\r\n",
1941                      __func__, __LINE__, chan);
1942       net_validate_dcw_state(iom_unit_idx, chan, NET_MAX_TALLY, "process_event-in_use=0");
1943       net_dev_state.want_to_read = 0;
1944       return;
1945     }
1946 
1947   /* Convert 8-bit packet to 36-bit words.
1948    * Buffer sized to match the ABSI read buffer_size (NET_MAX_TALLY words).
1949    * 3 words for IMP leader + up to NET_MAX_TALLY-3 words for IP data. */
1950   word36 buffer[NET_MAX_TALLY];
1951   uint words_processed = NET_MAX_TALLY;
1952   (void)memset(buffer, 0, sizeof(buffer));
1953 
1954   pkt8_to_word36(pkt8, pktlen, buffer, NET_MAX_TALLY);
1955 
1956   sim_debug(DBG_DEBUG, &net_dev,
1957             "%s: received %d bytes from gateway for unit %d chan %d\r\n",
1958             __func__, pktlen, iom_unit_idx, chan);
1959 
1960 # if defined(TESTING)
1961   if (sim_deb && (net_dev.dctrl & DBG_DEBUG))
1962     {
1963       dumppkt("Gateway-Read", buffer, words_processed);
1964     }
1965 # endif
1966 
1967   /* Validate the IOM DMA target PTW before writing packet data.
1968    *
1969    * This is a belt-and-suspenders check that runs AFTER the in_use guard
1970    * above.  If in_use=true, the workspace IS pinned and DDCW_ADDR should
1971    * be valid - but if the DCW list became corrupted (e.g., circular-buffer
1972    * wrap producing a bad DDCW), DDCW_ADDR might point outside the workspace.
1973    * CORRECTNESS NOTE (2026-08, unresolved): this comment previously claimed
1974    * "the workspace has only 1 IOM page (958 words for n_buffers=4,
1975    * buffer_size=228)".  That is very likely wrong: absi_io_.pl1 requests
1976    * WS_SIZE=4096 words (4 pages) via ioi_$workspace regardless of
1977    * buffer_size, and db.read.n_buffers = (WS_SIZE-2)/(buffer_size+3+
1978    * size(istat)) -- for buffer_size=228 that's roughly 17 buffers, not 4,
1979    * spanning close to the full 4096-word/4-page workspace even in normal,
1980    * uncorrupted operation.  If so, "DDCW_ADDR >= 01400 octal (page 1+) has
1981    * no PTW" and "page 3 access always means corruption" are not reliable
1982    * as written -- legitimate high-numbered buffers may validly land on
1983    * pages 1-3.  Not corrected here since the exact size(istat) (and hence
1984    * the true n_buffers) hasn't been verified precisely enough to assert a
1985    * replacement claim with confidence.  net_check_dma_ptw() itself is fine
1986    * regardless (it validates whatever pages DDCW_ADDR+tally actually span,
1987    * not a hardcoded page count) -- only this comment's narrative, and any
1988    * reasoning elsewhere that assumes "page 1+ is always corrupt", should be
1989    * treated with suspicion until this is verified.
1990    *
1991    * Calling iom_indirect_data_service() with PTW=0 would compute physical
1992    * address ~= 0 and overwrite the IOM mailbox area (addresses 0-0777),
1993    * causing an IOM interrupt storm -> Multics CPU at 100%.
1994    *
1995    * If invalid: close the gateway socket (discarding all queued NOOPs from the
1996    * kernel receive buffer), send one clean terminate interrupt so Multics
1997    * can re-establish the channel, and return without doing the DMA.
1998    *
1999    * max_words=NET_MAX_TALLY: cap the PTW range check at NET_MAX_TALLY words
2000    * (generous upper bound for any gateway packet).  This prevents false
2001    * positives when DDCW_TALLY=0 (IOM interprets as 4096 words, spanning
2002    * pages 0-3) which can occur if iom_continue_channel left the channel in a
2003    * transitional "uff or nothing to send" state.                          */
2004   if (!net_check_dma_ptw(iom_unit_idx, chan, NET_MAX_TALLY))
2005     {
2006       (void)sir_warn("%s:%d: invalid DMA PTW on chan %d - closing gateway socket and "
2007                      "sending terminate interrupt to allow Multics channel recovery",
2008                      __func__, __LINE__, chan);
2009       net_dev_state.want_to_read = 0;
2010       net_dev_state.ptw_failed_at = time(NULL); /* start reconnect backoff */
2011       /* Close the socket.  From the RECEIVER side, close() discards all
2012        * unread data in the kernel receive buffer.  This eliminates the
2013        * remaining queued gateway packets (typically 30 keepalive NOOPs) that
2014        * would otherwise continue triggering failed delivery attempts and
2015        * rapid terminate-interrupt storms after Multics re-issues the READ.
2016        * The gateway detects the closed connection and reconnects; NAK recovery
2017        * then re-synchronizes the NET frame counters.                      */
2018       if (net_dev_state.gateway_socket >= 0)
2019         {
2020           close(net_dev_state.gateway_socket);
2021           net_dev_state.gateway_socket = -1;
2022         }
2023       send_terminate_interrupt(net_dev_state.read_unit_idx,
2024                                net_dev_state.read_unit_chan);
2025       return;
2026     }
2027 
2028   /* Validate DDCW_ADDR is within the read channel's data buffer area.
2029    *
2030    * ROOT CAUSE FIX for the DDCW_ADDR=0 / DCW-list corruption bug:
2031    *
2032    * After all 4 buffers are delivered, iom_continue_channel reaches the TDCW
2033    * at DCW list offset 41 (rws.tdcw in absi_io_.pl1).  iom_list_service calls
2034    * unpack_DCW for the TDCW word; since the TDCW has DATA_ADDRESS=0 (all zero
2035    * bits, as initialized by absi_io_: "string(rws.tdcw)=""b; rws.tdcw.address
2036    * = rel(db.read.listp); rws.tdcw.type = "10"b"), unpack_DCW stores
2037    * p->DDCW_ADDR = 0.  LPW_TALLY then decrements to 0, setting uff=true.
2038    * iom_continue_channel sees uff=true, logs "uff or nothing to send", and
2039    * returns WITHOUT calling net_cmd(001) and WITHOUT sending a terminate
2040    * interrupt.  want_to_read=1 persists with p->DDCW_ADDR=0.
2041    *
2042    * The PTW check above passes for DDCW_ADDR=0 with max_words=NET_MAX_TALLY
2043    * because workspace page 0 IS a valid mapped page regardless of how many
2044    * IOM pages the workspace actually spans.  Without this range check,
2045    * iom_indirect_data_service(write=true) would write NET_MAX_TALLY words
2046    * of gateway packet data to workspace offset 0, overwriting the statq
2047    * and rss_idcw control structures.
2048    * The corrupted DCW list then causes downstream DDCW_TALLY=0 readings
2049    * in iom_list_service, triggering the 4096-word IDS walk -> thousands of
2050    * fetch_IDSPTW sir_warn calls.
2051    *
2052    * Fix: reject any DDCW_ADDR below the first valid data buffer offset.
2053    * The minimum valid address is NET_FIRST_BUFFER_OFFSET (43), which is the
2054    * start of buffer(0) in the workspace.  Send a terminate interrupt so
2055    * Multics re-issues ioi_$connect; the fresh net_cmd(001) / get_ddcw() call
2056    * will advance through the TDCW wrap back to IDCW[0]/DDCW[0] and set
2057    * DDCW_ADDR = 22 as expected.                                            */
2058 
2059   if ((int)p->DDCW_ADDR < NET_FIRST_BUFFER_OFFSET)
2060     {
2061       (void)sir_warn("%s:%d: DDCW_ADDR=%d on chan %d is below first buffer offset %d"
2062                      " (stale DDCW_ADDR; TAL fix prevents TDCW wrap case)"
2063                      " - sending terminate interrupt; gateway socket stays connected",
2064                      __func__, __LINE__, p->DDCW_ADDR, chan,
2065                      NET_FIRST_BUFFER_OFFSET);
2066       (void)sir_warn("%s:%d: DCW=%llo, IS_IDCW=%d, IS_TDCW=%d, IS_IOTD=%d, IS_IONTP=%d.",
2067                      __func__, __LINE__, p->DCW, IS_IDCW(p), IS_TDCW(p), IS_IOTD(p), IS_IONTP(p));
2068 
2069       /* Do NOT close the gateway socket.  The TDCW wrap is a normal, periodic
2070        * IOM event (happens after every 4th buffer delivery).  The socket
2071        * is healthy - only the IOM channel state needs resetting.  Closing
2072        * the socket would cause unnecessary gateway reconnect cycles (~every 4s)
2073        * which accumulate Multics error counts and eventually trigger channel
2074        * masking.  A terminate interrupt is sufficient: Multics re-issues
2075        * ioi_$connect, the fresh net_cmd(001)/get_ddcw() sets a valid
2076        * DDCW_ADDR (>= 43), and delivery resumes on the next gateway packet.   */
2077       net_dev_state.want_to_read = 0;
2078       send_terminate_interrupt(net_dev_state.read_unit_idx,
2079                                net_dev_state.read_unit_chan);
2080       return;
2081     }
2082 
2083 # if defined(DPS8_NET_DIAG)
2084   /* Diagnostic instrumentation: count every packet that makes it all the
2085    * way past every guard above (want_to_read, masked, gateway-connected,
2086    * in_use, DMA PTW, DDCW_ADDR) and is about to be actually written into
2087    * Multics's IOM workspace. Compare against how many packets the gateway
2088    * itself logs sending to this destination -- if this count is lower,
2089    * packets are being silently absorbed by one of the early, silent
2090    * returns above (most likely the want_to_read gate) without ever
2091    * reaching here. Rate-limited to a periodic summary -- this fires on
2092    * every packet including ordinary background traffic, not just the
2093    * connection under test, so logging every one floods the console.       */
2094   {
2095     static unsigned long delivered_count = 0;
2096     static time_t last_delivered_log = 0;
2097     delivered_count++;
2098     time_t now_dc = time(NULL);
2099     if (now_dc - last_delivered_log >= 2)
2100       {
2101         (void)sir_warn("%s:%d: delivered so far=%lu (last %d bytes) chan %d\r\n",
2102                        __func__, __LINE__delivered_count, pktlen, chan);
2103         fflush(stdout);
2104         last_delivered_log = now_dc;
2105       }
2106   }
2107 # endif
2108 
2109   /* Write the packet to the current IOM workspace buffer[N]. */
2110   iom_indirect_data_service(
2111       iom_unit_idx, chan, buffer, &words_processed, true);
2112 
2113   sim_debug(DBG_DEBUG, &net_dev,
2114             "%s: wrote %d words to IOM buffer, advancing channel and sending marker interrupt\r\n",
2115             __func__, words_processed);
2116 
2117   /*
2118    * Advance the channel to workspace buffer[N+1] and send a marker
2119    * interrupt.  iom_continue_channel() does the following in order:
2120    *
2121    *   1. Calls iom_list_service() to fetch IDCW[N+1] from the DCW list
2122    *      (LPW_DCW_PTR advances from IDCW[N+1] to DDCW[N+1]).
2123    *   2. Calls d->iom_cmd() for IDCW[N+1]:
2124    *        net_cmd(read) -> get_ddcw() -> iom_list_service() reads DDCW[N+1]
2125    *        -> DDCW_ADDR = buffer[N+1], LPW_DCW_PTR = IDCW[N+2].
2126    *        net_cmd sets want_to_read=1 and returns IOM_CMD_PENDING.
2127    *   3. Calls send_marker_interrupt():
2128    *        stat.offset = LPW_offset(IDCW[N+2]) - 1 = 2*(N+2) - 1
2129    *        stop_buffer = divide(stat.offset, 2) = N+1
2130    *      absi_io_'s process_read_status fires, advances runx, and
2131    *      calls connect to keep the channel running.
2132    *
2133    * start_io sees running=true (marker) and does NOT reconnect; the
2134    * channel stays pending with DDCW_ADDR pointing to buffer[N+1].
2135    * want_to_read remains 1 (set inside net_cmd via iom_continue_channel).
2136    *
2137    * On failure (e.g. DCW list corrupt, "expected IDCW"): iom_continue_channel
2138    * just returns without calling net_cmd and without sending any interrupt.
2139    * We detect this via delivery_succeeded: net_cmd(READ) sets it to 1 on
2140    * success; we clear it just before calling iom_continue_channel.  After
2141    * 3 consecutive failures we reset want_to_read, close the gateway socket,
2142    * and send a terminate interrupt so Multics can re-establish the channel.
2143    */
2144   net_dev_state.delivery_succeeded = 0;
2145   int rc = iom_continue_channel(iom_unit_idx, chan);
2146 
2147   /* Detect and break "expected IDCW" cascade. */
2148   {
2149     static int consecutive_iom_failures = 0;
2150     if (net_dev_state.delivery_succeeded)
2151       {
2152         consecutive_iom_failures = 0;
2153       }
2154     else
2155       {
2156         if (++consecutive_iom_failures >= 3)
2157           {
2158             (void)sir_warn("%s:%d: %d consecutive IOM delivery failures on chan %d; "
2159                            "resetting want_to_read, closing gateway socket, and "
2160                            "sending terminate interrupt\r\n",
2161                            __func__, __LINE__, consecutive_iom_failures,
2162                            net_dev_state.read_unit_chan);
2163             consecutive_iom_failures = 0;
2164             net_dev_state.want_to_read = 0;
2165             /* Close socket to discard remaining queued gateway packets;
2166              * see the comment in the PTW-check block above.           */
2167             if (net_dev_state.gateway_socket >= 0)
2168               {
2169                 close(net_dev_state.gateway_socket);
2170               }
2171             net_dev_state.gateway_socket = -1;
2172             if (rc == 0) /* we handle the rc != 0 case below */
2173               send_terminate_interrupt(net_dev_state.read_unit_idx,
2174                                        net_dev_state.read_unit_chan);
2175           }
2176       }
2177   }
2178 
2179   /* if iom_continue_channel returned a fatal error, terminate the I/O and let the DCM
2180      restart it. */
2181   if (rc != 0)
2182     {
2183       send_terminate_interrupt(net_dev_state.read_unit_idx,
2184                                net_dev_state.read_unit_chan);
2185     }
2186 }
2187 
2188 #endif /* if defined(WITH_NET_DEV) */

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