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