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

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