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