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