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

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