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

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