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