1 /*
2 * vim: filetype=c:tabstop=4:ai:expandtab
3 * SPDX-License-Identifier: ICU
4 * scspell-id: 35c9cf5c-ebbf-11ed-8d34-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 MGP packets between the IOM (Multics)
22 // and an external NCP process (multics_ncp) via a Unix domain socket.
23 //
24 // The NCP process handles all Chaosnet protocol logic: connection management,
25 // flow control, retransmission, and endpoint ID mapping.
26 //
27 // Communication protocol with the NCP:
28 // - Single bidirectional Unix domain socket (default: /tmp/mgp_ncp)
29 // - Length-prefixed framing: [2-byte BE length][MGP 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 -> NCP): Read 36-bit words from IOM, convert to 8-bit,
33 // send length-prefixed to NCP, return IOM_CMD_DISCONNECT (terminate).
34 //
35 // Read (NCP -> Multics): Set want_to_read flag, return IOM_CMD_PENDING.
36 // In mgp_process_event(), poll NCP 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 <stdarg.h>
42 #include <ctype.h>
43 #include <unistd.h>
44 #include <stdint.h>
45 #include <errno.h>
46 #include <fcntl.h>
47
48 #include <sys/types.h>
49 #include <time.h>
50 #include <sys/time.h>
51
52 #include "dps8.h"
53 #include "dps8_sir.h"
54 #include "dps8_iom.h"
55 #include "dps8_mgp.h"
56 #include "dps8_sys.h"
57 #include "dps8_cable.h"
58 #include "dps8_cpu.h"
59 #include "dps8_faults.h"
60 #include "dps8_scu.h"
61 #include "dps8_utils.h"
62
63 #include <uv.h>
64
65 #if defined(THREADZ) || defined(LOCKLESS)
66 # include "threadz.h"
67 #endif
68
69 #if defined(NO_LOCALE)
70 # define xstrerror_l strerror
71 #endif
72
73 #if defined(FREE)
74 # undef FREE
75 #endif /* if defined(FREE) */
76 #define FREE(p) do \
77 { \
78 free((p)); \
79 (p) = NULL; \
80 } while(0)
81
82 #if defined(WITH_MGP_DEV)
83
84 # define DBG_CTR 1
85
86 // Path to NCP process Unix socket (configurable via SET MGP SOCKET=<path>)
87 # define NCP_SOCKET_PATH_DEFAULT "/tmp/mgp_ncp"
88 # define NCP_SOCKET_PATH_MAX 108
89
90 static char ncp_socket_path[NCP_SOCKET_PATH_MAX] = NCP_SOCKET_PATH_DEFAULT;
91
92 // Number of words in an MGP packet header (4 words x 36 bits = 16 x 9-bit bytes)
93 # define MGP_PACKET_HEADER_SIZE 4
94
95 // Maximum data bytes in an MGP packet
96 # define MGP_MAX_DATA 488
97
98 // Frame header size for NCP communication (2-byte big-endian length prefix)
99 # define NCP_FRAME_HEADER_SIZE 2
100
101 // Maximum 8-bit packet size (16 header + 488 data)
102 # define MAX_PKT_BYTES (16 + MGP_MAX_DATA)
103
104 /* Maximum plausible DDCW_TALLY for any MGP channel buffer, in 36-bit words.
105 * The read channel uses data_size=129 and the write channel uses data_size=128
106 * (set by initialize_workspace in mgp_read_dcm_.pl1 / mgp_write_dcm_.pl1).
107 * MAX_PKT_BYTES/4 = 126 words for the payload plus ~4 header words ~= 130 max.
108 * We use 256 as a generous upper bound so this check remains valid even if the
109 * Multics buffer size is changed, as long as it stays within one IOM page
110 * (the ioi_ workspace is 2000 octal = 1024 words = one page).
111 * TALLY=0 (IOM convention for 4096) and TALLY > MGP_MAX_TALLY both indicate
112 * DCW corruption; see the validation checks in mgp_cmd cases 001 and 011. */
113 # define MGP_MAX_TALLY 256
114
115 /* Backoff after a PTW failure: do not reconnect for this many seconds.
116 * When Multics's memory manager pages out the MGP daemon's IOM DMA buffer
117 * pages after overnight idle, the PTW check fails. Without a backoff the
118 * terminate-interrupt -> mgp_cmd(001) -> ncp_connect cycle repeats every
119 * ~60 ms (too fast for the memory manager to page the data back in).
120 * The backoff check lives in ncp_connect() so it catches ALL reconnect
121 * paths, including ncp_send_packet() (Multics WRITE cmd) which previously
122 * bypassed the per-event check and caused ~91 ms reconnect cycles. */
123 # define PTW_BACKOFF_SECS 15
124
125 /* Timeout before declaring the channel "masked+stuck" and forcing recovery.
126 * See masked_since in mgp_dev_state and the masked-channel check in
127 * mgp_process_event for details. */
128 # define MASKED_STUCK_TIMEOUT_SECS 30
129
130 /* Timeout before releasing an IOM channel that has been waiting for the NCP
131 * to connect. Keeps the channel from staying in IOM_CMD_PENDING indefinitely
132 * when multics_ncp is not running. */
133 # define NCP_ABSENT_TIMEOUT_SECS 30
134
135 /* Timeout before declaring "no forward progress" and forcing a hard socket
136 * reset, independent of the masked/want_to_read state.
137 *
138 * MASKED_STUCK_TIMEOUT_SECS (above) only accumulates while the channel is
139 * continuously seen masked across successive mgp_process_event() calls.
140 * ioi_masked$timer's mask_channel (Multics's own channel-timeout handler,
141 * ioi_masked.pl1) both masks the channel AND immediately issues a dummy
142 * "unmask connect" (reset-status IDCW) in the same call when the device is
143 * multiplexed. That reconnect can flip want_to_read back to 1 for one
144 * mgp_process_event() tick, which resets masked_since to 0 (see the
145 * "channel is not masked" branch below) before MASKED_STUCK_TIMEOUT_SECS is
146 * ever reached -> so a channel that is genuinely wedged in a repeating
147 * mask/unmask timeout loop (observed at ~4s intervals against Multics's
148 * short mgpr device read timeout) can defeat that safety net indefinitely.
149 *
150 * This second, independent watchdog tracks wall-clock time since the last
151 * *successfully delivered* packet (last_progress_time), updated only when a
152 * packet is actually written into Multics's workspace. It is immune to the
153 * masked/unmask flicker because it does not depend on observing !masked in
154 * between: it fires purely on elapsed time without any real delivery. */
155 # define NO_PROGRESS_TIMEOUT_SECS 60
156
157 /* Minimum valid DDCW_ADDR for the MGP read channel workspace.
158 *
159 * The read channel workspace layout (mgp_read_dcm_.pl1, buffer_size=6,
160 * data_size=129):
161 * offsets 0-11: DCW list (6 x IDCW + 6 x DDCW)
162 * offset 12: TDCW (circular wrap; DATA_ADDRESS=0)
163 * offset 13: reset_idcw
164 * offsets 14-21: status_queue (4 x istat = 8 words)
165 * offset 22+ : buffer(0..5) data areas (129 words each)
166 *
167 * Any DDCW_ADDR < 22 is invalid (it points into the DCW list or control
168 * structures, not into a data buffer). DDCW_ADDR=0 in particular is set
169 * by iom_list_service when it processes the TDCW at offset 12 via
170 * unpack_DCW, which stores the TDCW's DATA_ADDRESS field (=0, all zero
171 * bits) into p->DDCW_ADDR.
172 */
173 # define MGP_FIRST_BUFFER_OFFSET 22
174
175 /* Bytes received from the NCP and not yet delivered to Multics. The NCP
176 * can send packets faster than Multics reads them (one per READ), so the
177 * buffer holds many frames, and when it is nearly full we stop reading from
178 * the socket (uv_read_stop) until Multics has consumed some of it. The
179 * socket applies backpressure rather than overflowing our buffer. It used
180 * to hold only two frames and was emptied and the connection closed when a
181 * burst overflowed it ("ncp_read_cb: receive buffer overflow"), which lost
182 * MGP frames and forced a reconnect. Reading stops while less than one
183 * max frame is free and starts again after half the buffer is free.
184 * On disconnect, the complete frames already received are still delivered
185 * before reconnection is allowed: they were received, so they shouldn't be
186 * lost just because Multics is behind, and they belong to the old session,
187 * so they can't follow frames of a new one. Frames are taken as fast as
188 * Multics reads (a full buffer at ~18 frames/s is about 3.5 s), and a
189 * full read ring holds them as it does while connected.
190 *
191 * The drain must end some time, since it blocks reconnection, so this is
192 * an intentional packet-loss boundary: if no frame has been taken for
193 * NCP_EOF_DRAIN_IDLE_NS (60 s), the rest are discarded and logged. No
194 * frame taken for that long means Multics hasn't read the ring or
195 * reconnected the channel in that time, not that it is merely slow.
196 * This matches the connected case, where the no-progress watchdog
197 * (NO_PROGRESS_TIMEOUT_SECS, also 60 s) resets the channel and loses the
198 * held packets after the same time; a RING_FULL hold counts as no
199 * progress in both.
200 */
201 # define NCP_MAX_FRAME (NCP_FRAME_HEADER_SIZE + MAX_PKT_BYTES)
202 # define NCP_IN_BUFFER_SIZE (64 * NCP_MAX_FRAME)
203 # define NCP_EOF_DRAIN_IDLE_NS 60000000000ULL /* 60 s without a frame taken */
204
205 static void mgp_init_dev_state(void);
206
207 # if defined(TESTING)
208 static void dumppkt(char *hdr, word36 *buf, uint words);
209 # endif
210
211 struct mgp_dev_state
212 {
213 uv_pipe_t *pipe; /* connected pipe to NCP process */
214 u_char in_buffer[NCP_IN_BUFFER_SIZE]; /* libuv read buffer */
215 int in_buffer_len; /* bytes buffered */
216 u_char reading_stopped; /* uv_read_stop called because buffer full */
217 u_char eof_draining; /* peer closed; deliver buffered frames first */
218 uint64_t eof_drain_progress; /* uv_hrtime() at EOF or the last frame taken */
219 u_char want_to_read; /* flag: Multics has a pending read */
220 uint read_unit_idx; /* saved IOM unit index for pending read */
221 uint read_unit_chan; /* saved IOM channel for pending read */
222 u_char delivery_succeeded; /* set by mgp_cmd(READ) when IOM accepts pkt */
223 time_t want_to_read_since; /* wall-clock time when want_to_read was set */
224 time_t ptw_failed_at; /* wall-clock time of last PTW check failure */
225 time_t masked_since; /* wall-clock time channel first seen masked while connected */
226 time_t last_progress_time; /* wall-clock time of last successful packet delivery to Multics;
227 0 means "not yet established" (lazily initialized) */
228 u_char ring_full; /* holding packets: Multics hasn't read the next buffer */
229 } mgp_dev_state;
230
231 static struct mgp_state
232 {
233 char device_name[MAX_DEV_NAME_LEN];
234 } mgp_state[N_MGP_UNITS_MAX];
235
236 # define N_MGP_UNITS 2 // default
237
238 # define UNIT_FLAGS \
239 ( UNIT_FIX | UNIT_ATTABLE | UNIT_ROABLE | UNIT_DISABLE | UNIT_IDLE )
240
241 UNIT mgp_unit[N_MGP_UNITS_MAX] = {
242 {
243 UDATA(NULL, UNIT_FLAGS, 0),
244 0, 0, 0, 0, 0,
245 NULL, NULL, NULL, NULL
246 }
247 };
248
249 # define MGP_UNIT_IDX(uptr) (( uptr ) - mgp_unit )
250
251 static DEBTAB mgp_dt[] = {
252 { "NOTIFY", DBG_NOTIFY, NULL },
253 { "INFO", DBG_INFO, NULL },
254 { "ERR", DBG_ERR, NULL },
255 { "WARN", DBG_WARN, NULL },
256 { "DEBUG", DBG_DEBUG, NULL },
257 { "ALL", DBG_ALL, NULL }, // Don't move as it messes up DBG message
258 { NULL, 0, NULL }
259 };
260
261 static t_stat
262 mgp_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)
*/
263 UNUSED const void *desc)
264 {
265 sim_printf("Number of MGP units in system is %d\r\n", mgp_dev.numunits);
266
267 return SCPE_OK;
268 }
269
270 static t_stat
271 mgp_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)
*/
272 UNUSED void *desc)
273 {
274 if (!cptr)
275 {
276 return SCPE_ARG;
277 }
278
279 int n = atoi(cptr);
280 if (n < 1 || n > N_MGP_UNITS_MAX)
281 {
282 return SCPE_ARG;
283 }
284
285 mgp_dev.numunits = (uint32)n;
286
287 return SCPE_OK;
288 }
289
290 static t_stat
291 mgp_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)
*/
292 UNUSED const void *desc)
293 {
294 int n = (int)MGP_UNIT_IDX(uptr);
295
296 if (n < 0 || n >= N_MGP_UNITS_MAX)
297 {
298 return SCPE_ARG;
299 }
300
301 if (mgp_state[n].device_name[1] != 0)
302 {
303 sim_printf("name : %s", mgp_state[n].device_name);
304 }
305 else
306 {
307 sim_printf("name : MGP%d", n);
308 }
309
310 return SCPE_OK;
311 }
312
313 static t_stat
314 mgp_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)
*/
315 UNUSED void *desc)
316 {
317 int n = (int)MGP_UNIT_IDX(uptr);
318
319 if (n < 0 || n >= N_MGP_UNITS_MAX)
320 {
321 return SCPE_ARG;
322 }
323
324 if (cptr)
325 {
326 strncpy(mgp_state[n].device_name, cptr, MAX_DEV_NAME_LEN - 1);
327 mgp_state[n].device_name[MAX_DEV_NAME_LEN - 1] = 0;
328 }
329 else
330 {
331 mgp_state[n].device_name[0] = 0;
332 }
333
334 return SCPE_OK;
335 }
336
337 static t_stat
338 mgp_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)
*/
339 UNUSED const void *desc)
340 {
341 sim_printf("NCP socket path: %s\r\n", ncp_socket_path);
342 return SCPE_OK;
343 }
344
345 static t_stat
346 mgp_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)
*/
347 UNUSED void *desc)
348 {
349 if (!cptr || strlen(cptr) == 0)
350 return SCPE_ARG;
351 if (strlen(cptr) >= NCP_SOCKET_PATH_MAX - 1)
352 {
353 sim_printf("NCP socket path too long (max %d chars)\r\n",
354 NCP_SOCKET_PATH_MAX - 1);
355 return SCPE_ARG;
356 }
357 strncpy(ncp_socket_path, cptr, NCP_SOCKET_PATH_MAX - 1);
358 ncp_socket_path[NCP_SOCKET_PATH_MAX - 1] = '\0';
359 sim_printf("MGP NCP socket path set to: %s\r\n", ncp_socket_path);
360 return SCPE_OK;
361 }
362
363 # define UNIT_WATCH UNIT_V_UF
364
365 static MTAB mgp_mod[] = {
366 # if !defined(SPEED)
367 { UNIT_WATCH, 1, "WATCH", "WATCH", 0, 0, NULL, NULL },
368 { UNIT_WATCH, 0, "NOWATCH", "NOWATCH", 0, 0, NULL, NULL },
369 # endif /* if !defined(SPEED) */
370 {
371 MTAB_XTD | MTAB_VDV | MTAB_NMO | MTAB_VALR, /* Mask */
372 0, /* Match */
373 "NUNITS", /* Print string */
374 "NUNITS", /* Match string */
375 mgp_set_nunits, /* Validation routine */
376 mgp_show_nunits, /* Display routine */
377 "Number of MGP units in the system", /* Value descriptor */
378 NULL /* Help */
379 },
380 {
381 MTAB_XTD | MTAB_VUN | MTAB_VALR | MTAB_NC, /* Mask */
382 0, /* Match */
383 "NAME", /* Print string */
384 "NAME", /* Match string */
385 mgp_set_device_name, /* Validation routine */
386 mgp_show_device_name, /* Display routine */
387 "Set the device name", /* Value descriptor */
388 NULL /* Help */
389 },
390 {
391 MTAB_XTD | MTAB_VDV | MTAB_VALR | MTAB_NC, /* Mask */
392 0, /* Match */
393 "SOCKET", /* Print string */
394 "SOCKET", /* Match string */
395 mgp_set_socket_path, /* Validation routine */
396 mgp_show_socket_path, /* Display routine */
397 "Unix socket path for NCP connection", /* Value descriptor */
398 NULL /* Help */
399 },
400 MTAB_eol
401 };
402
403 static t_stat
404 mgp_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)
*/
405 {
406 return SCPE_OK;
407 }
408
409 static t_stat
410 mgpAttach(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)
*/
411 {
412 if (!cptr)
413 {
414 return SCPE_ARG;
415 }
416
417 // If we're already attached, then detach ...
418 if (( uptr->flags & UNIT_ATT ) != 0)
419 {
420 detach_unit(uptr);
421 }
422
423 uptr->flags |= UNIT_ATT;
424
425 return SCPE_OK;
426 }
427
428 // Detach (connect) ...
429 static t_stat
430 mgpDetach(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)
*/
431 {
432 if (( uptr->flags & UNIT_ATT ) == 0)
433 {
434 return SCPE_OK;
435 }
436
437 uptr->flags &= ~(unsigned int)UNIT_ATT;
438
439 return SCPE_OK;
440 }
441
442 DEVICE mgp_dev = {
443 "MGP", /* Name */
444 mgp_unit, /* Units */
445 NULL, /* Registers */
446 mgp_mod, /* Modifiers */
447 N_MGP_UNITS, /* #units */
448 10, /* Address radix */
449 24, /* Address width */
450 1, /* Address increment */
451 8, /* Data radix */
452 36, /* Data width */
453 NULL, /* Examine */
454 NULL, /* Deposit */
455 mgp_reset, /* Reset */
456 NULL, /* Boot */
457 mgpAttach, /* Attach */
458 mgpDetach, /* Detach */
459 NULL, /* Context */
460 DEV_DEBUG, /* Flags */
461 0, /* Debug control flags */
462 mgp_dt, /* Debug flag names */
463 NULL, /* Memory size change */
464 NULL, /* Logical name */
465 NULL, /* Help */
466 NULL, /* Attach help */
467 NULL, /* Attach context */
468 NULL, /* Description */
469 NULL /* End */
470 };
471
472 /*
473 * mgp_init()
474 */
475
476 // Once-only initialization
477
478 void
479 mgp_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)
*/
480 {
481 (void)memset(mgp_state, 0, sizeof ( mgp_state ));
482 mgp_init_dev_state();
483 }
484
485 static void
486 mgp_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)
*/
487 {
488 (void)memset(&mgp_dev_state, 0, sizeof ( mgp_dev_state ));
489 mgp_dev_state.pipe = NULL;
490 mgp_dev_state.in_buffer_len = 0;
491 mgp_dev_state.reading_stopped = 0;
492 mgp_dev_state.eof_draining = 0;
493 }
494
495 static void ncp_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)
*/
496 free(handle);
497 }
498
499 static int ncp_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)
*/
500 int pos = 0;
501 int count = 0;
502
503 while (mgp_dev_state.in_buffer_len - pos >= NCP_FRAME_HEADER_SIZE)
504 {
505 int len = ((unsigned char)mgp_dev_state.in_buffer[pos] << 8) |
506 (unsigned char)mgp_dev_state.in_buffer[pos + 1];
507
508 if (len <= 0 || len > MAX_PKT_BYTES ||
509 mgp_dev_state.in_buffer_len - pos < NCP_FRAME_HEADER_SIZE + len)
510 break;
511
512 pos += NCP_FRAME_HEADER_SIZE + len;
513 count++;
514 }
515
516 *complete_bytes = pos;
517
518 return count;
519 }
520
521 static void ncp_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)
*/
522 int complete_bytes;
523 int frames = ncp_complete_frames(&complete_bytes);
524
525 if (complete_bytes != mgp_dev_state.in_buffer_len)
526 (void)sir_warn("NCP: discarding %d incomplete/invalid trailing bytes at disconnect",
527 mgp_dev_state.in_buffer_len - complete_bytes);
528
529 mgp_dev_state.in_buffer_len = complete_bytes;
530 mgp_dev_state.reading_stopped = 0;
531 mgp_dev_state.eof_draining = frames != 0;
532 mgp_dev_state.eof_drain_progress = uv_hrtime();
533 }
534
535 static void ncp_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)
*/
536 if (!mgp_dev_state.eof_draining)
537 return;
538
539 if (mgp_dev_state.in_buffer_len == 0)
540 {
541 mgp_dev_state.eof_draining = 0;
542 return;
543 }
544
545 if (uv_hrtime() - mgp_dev_state.eof_drain_progress >= NCP_EOF_DRAIN_IDLE_NS)
546 {
547 int complete_bytes;
548 int frames = ncp_complete_frames(&complete_bytes);
549 (void)sir_warn("NCP: no frame taken for %llu s while draining after disconnect; discarding %d frames (%d bytes)",
550 (unsigned long long) (NCP_EOF_DRAIN_IDLE_NS / 1000000000ULL),
551 frames, mgp_dev_state.in_buffer_len);
552 mgp_dev_state.in_buffer_len = 0;
553 mgp_dev_state.eof_draining = 0;
554 }
555 }
556
557 static void ncp_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)
*/
558 (void)handle;
559 size_t free_bytes = sizeof(mgp_dev_state.in_buffer) - (size_t)mgp_dev_state.in_buffer_len;
560 buf->base = (char *)mgp_dev_state.in_buffer + mgp_dev_state.in_buffer_len;
561 buf->len = free_bytes < suggested_size ? free_bytes : suggested_size;
562 }
563
564 static void ncp_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)
*/
565 if (!mgp_dev_state.reading_stopped &&
566 (int)sizeof(mgp_dev_state.in_buffer) - mgp_dev_state.in_buffer_len < NCP_MAX_FRAME) {
567 int rc = uv_read_stop(stream);
568
569 if (rc == 0) {
570 mgp_dev_state.reading_stopped = 1;
571 } else {
572 (void)sir_error("NCP: uv_read_stop failed: %s", uv_strerror(rc));
573 uv_close((uv_handle_t *)stream, ncp_close_cb);
574 mgp_dev_state.pipe = NULL;
575 ncp_begin_eof_drain();
576 }
577 }
578 }
579
580 static void ncp_alloc_cb(uv_handle_t *handle, size_t suggested_size, uv_buf_t *buf);
581 static void ncp_read_cb(uv_stream_t *stream, ssize_t nread, const uv_buf_t *buf);
582
583 static void ncp_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)
*/
584 if (mgp_dev_state.reading_stopped && mgp_dev_state.pipe &&
585 mgp_dev_state.in_buffer_len <= (int)sizeof(mgp_dev_state.in_buffer) / 2) {
586 int rc = uv_read_start((uv_stream_t *)mgp_dev_state.pipe,
587 ncp_alloc_cb, ncp_read_cb);
588
589 if (rc == 0) {
590 mgp_dev_state.reading_stopped = 0;
591 } else {
592 (void)sir_error("NCP: uv_read_start failed: %s", uv_strerror(rc));
593 uv_close((uv_handle_t *)mgp_dev_state.pipe, ncp_close_cb);
594 mgp_dev_state.pipe = NULL;
595 ncp_begin_eof_drain();
596 }
597 }
598 }
599
600 static void ncp_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)
*/
601 (void)buf; /* points into in_buffer; see ncp_alloc_cb */
602 if (nread == UV_ENOBUFS) {
603 ncp_stop_reading_if_full(stream);
604 return;
605 }
606 if (nread < 0) {
607 if (nread != UV_EOF)
608 (void)sir_error("ncp_read_cb: %s", uv_strerror(nread));
609 else
610 (void)sir_info("%s:%d: NCP connection closed", __func__, __LINE__);
611 uv_close((uv_handle_t *)stream, ncp_close_cb);
612 mgp_dev_state.pipe = NULL;
613 ncp_begin_eof_drain();
614 return;
615 }
616 mgp_dev_state.in_buffer_len += (int)nread;
617 ncp_stop_reading_if_full(stream);
618 }
619
620 static void ncp_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)
*/
621 if (status < 0) {
622 (void)sir_notice("%s: connect error: %s", __func__, uv_strerror(status));
623 if (mgp_dev_state.pipe) {
624 uv_close((uv_handle_t *)mgp_dev_state.pipe, ncp_close_cb);
625 mgp_dev_state.pipe = NULL;
626 }
627 } else {
628 (void)sir_info("%s:%d: connected to NCP at %s",
629 __func__, __LINE__, ncp_socket_path);
630 mgp_dev_state.reading_stopped = 0;
631 int rc = uv_read_start((uv_stream_t *)mgp_dev_state.pipe,
632 ncp_alloc_cb, ncp_read_cb);
633 if (rc < 0) {
634 (void)sir_error("NCP: initial uv_read_start failed: %s", uv_strerror(rc));
635 uv_close((uv_handle_t *)mgp_dev_state.pipe, ncp_close_cb);
636 mgp_dev_state.pipe = NULL;
637 }
638 }
639 free(req);
640 }
641
642 /*
643 * Connect to the NCP process via Unix domain socket (async).
644 * Returns 0 if a connect has been initiated (result arrives via callback),
645 * -1 if still in PTW backoff or allocation failed.
646 */
647 static int
648 ncp_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)
*/
649 {
650 if (mgp_dev_state.pipe) return 0; /* already connected or pending */
651 if (mgp_dev_state.eof_draining) return -1;
652
653 /* PTW-failure backoff */
654 if (mgp_dev_state.ptw_failed_at > 0)
655 {
656 if (time(NULL) - mgp_dev_state.ptw_failed_at < PTW_BACKOFF_SECS)
657 return -1;
658 mgp_dev_state.ptw_failed_at = 0;
659 }
660
661 mgp_dev_state.pipe = malloc(sizeof(uv_pipe_t));
662 if (!mgp_dev_state.pipe)
663 return -1;
664 uv_pipe_init(uv_default_loop(), mgp_dev_state.pipe, 0);
665
666 uv_connect_t *req = malloc(sizeof(uv_connect_t));
667 if (!req) {
668 uv_close((uv_handle_t *)mgp_dev_state.pipe, ncp_close_cb);
669 mgp_dev_state.pipe = NULL;
670 return -1;
671 }
672
673 mgp_dev_state.in_buffer_len = 0;
674 mgp_dev_state.reading_stopped = 0;
675 mgp_dev_state.eof_draining = 0;
676 uv_pipe_connect(req, mgp_dev_state.pipe, ncp_socket_path, ncp_connect_cb);
677 return 0;
678 }
679
680 typedef struct {
681 uv_write_t req;
682 uv_buf_t buf;
683 } ncp_write_req_t;
684
685 static void ncp_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)
*/
686 ncp_write_req_t *wr = (ncp_write_req_t *)req;
687 if (status < 0)
688 (void)sir_error("ncp_write_cb: %s", uv_strerror(status));
689 free(wr->buf.base);
690 free(wr);
691 }
692
693 /*
694 * Send a length-prefixed 8-bit MGP packet to the NCP process (async).
695 * Returns 0 on success, -1 on failure.
696 */
697 static int
698 ncp_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)
*/
699 {
700 # if defined(THREADZ) || defined(LOCKLESS)
701 lock_libuv();
702 # endif
703 if (!mgp_dev_state.pipe) {
704 if (ncp_connect() < 0) {
705 # if defined(THREADZ) || defined(LOCKLESS)
706 unlock_libuv();
707 # endif
708 return -1;
709 }
710 }
711
712 ncp_write_req_t *wr = malloc(sizeof(ncp_write_req_t));
713 if (!wr) {
714 # if defined(THREADZ) || defined(LOCKLESS)
715 unlock_libuv();
716 # endif
717 return -1;
718 }
719 wr->buf.len = NCP_FRAME_HEADER_SIZE + pktlen;
720 wr->buf.base = malloc(wr->buf.len);
721 if (!wr->buf.base) {
722 free(wr);
723 # if defined(THREADZ) || defined(LOCKLESS)
724 unlock_libuv();
725 # endif
726 return -1;
727 }
728 wr->buf.base[0] = (u_char)((pktlen >> 8) & 0xFF);
729 wr->buf.base[1] = (u_char)(pktlen & 0xFF);
730 memcpy(wr->buf.base + NCP_FRAME_HEADER_SIZE, pkt8, pktlen);
731
732 int rc = uv_write(&wr->req, (uv_stream_t *)mgp_dev_state.pipe,
733 &wr->buf, 1, ncp_write_cb);
734 if (rc < 0) {
735 (void)sir_error("ncp_send_packet: uv_write: %s", uv_strerror(rc));
736 free(wr->buf.base);
737 free(wr);
738 # if defined(THREADZ) || defined(LOCKLESS)
739 unlock_libuv();
740 # endif
741 return -1;
742 }
743 # if defined(THREADZ) || defined(LOCKLESS)
744 unlock_libuv();
745 # endif
746 return 0;
747 }
748
749 /*
750 * Consume one length-prefixed packet from the libuv receive buffer.
751 * Returns byte count >0, 0 if not enough data yet, and -1 on error.
752 * Data is deposited into in_buffer by ncp_read_cb via libuv async.
753 */
754 static int
755 ncp_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)
*/
756 {
757 if (!mgp_dev_state.pipe && !mgp_dev_state.eof_draining)
758 return 0; /* connect pending; data arrives via ncp_read_cb */
759
760 if (mgp_dev_state.in_buffer_len < NCP_FRAME_HEADER_SIZE) return 0;
761
762 int pktlen = ((unsigned char)mgp_dev_state.in_buffer[0] << 8)
763 | (unsigned char)mgp_dev_state.in_buffer[1];
764
765 if (pktlen <= 0 || pktlen > maxlen) {
766 (void)sir_error("%s:%d: bad packet length %d -- resetting NCP connection",
767 __func__, __LINE__, pktlen);
768 if (mgp_dev_state.pipe) {
769 uv_close((uv_handle_t *)mgp_dev_state.pipe, ncp_close_cb);
770 mgp_dev_state.pipe = NULL;
771 }
772 mgp_dev_state.in_buffer_len = 0;
773 mgp_dev_state.eof_draining = 0;
774 return -1;
775 }
776
777 if (mgp_dev_state.in_buffer_len < NCP_FRAME_HEADER_SIZE + pktlen) return 0;
778
779 memcpy(pkt8, mgp_dev_state.in_buffer + NCP_FRAME_HEADER_SIZE, pktlen);
780 int consumed = NCP_FRAME_HEADER_SIZE + pktlen;
781 mgp_dev_state.in_buffer_len -= consumed;
782 if (mgp_dev_state.in_buffer_len > 0)
783 memmove(mgp_dev_state.in_buffer,
784 mgp_dev_state.in_buffer + consumed,
785 mgp_dev_state.in_buffer_len);
786
787 if (mgp_dev_state.eof_draining)
788 {
789 mgp_dev_state.eof_drain_progress = uv_hrtime();
790 if (mgp_dev_state.in_buffer_len == 0)
791 mgp_dev_state.eof_draining = 0;
792 }
793
794 return pktlen;
795 }
796
797 /*
798 * Convert an 8-bit MGP packet to 36-bit IOM words.
799 * The 16-byte header occupies 4 words (4 x 9-bit bytes per word).
800 * Data bytes follow in 9-bit format (upper bit zero).
801 */
802 static void
803 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)
*/
804 {
805 uint j;
806 (void)memset(buf, 0, maxwords * sizeof(word36));
807
808 for (j = 0; j < (uint)pktlen && j / 4 < maxwords; j++)
809 {
810 putbits36_9(&buf[j / 4], (j % 4) * 9, pkt8[j]);
811 }
812 }
813
814 /*
815 * Convert 36-bit IOM words to an 8-bit MGP packet.
816 * Returns the total packet length in bytes.
817 */
818 static int
819 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)
*/
820 {
821 int j;
822 int total = (int)(words * 4);
823 if (total > maxlen)
824 {
825 total = maxlen;
826 }
827
828 for (j = 0; j < total; j++)
829 {
830 pkt8[j] = getbits36_9(buf[j / 4], (j % 4) * 9);
831 }
832
833 /* Determine actual packet length from byte_count in header */
834 if (total >= 16)
835 {
836 int byte_count = (pkt8[8] & 0xFF) | ((pkt8[9] & 0xFF) << 8);
837 int real_len = 16 + byte_count;
838 if (real_len < total)
839 {
840 total = real_len;
841 }
842 }
843
844 return total;
845 }
846
847 static iom_cmd_rc_t
848 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)
*/
849 uint expected_tally, uint *tally)
850 {
851 # if defined(TESTING)
852 cpu_state_t * cpup = _cpup;
853 # endif
854 bool send, uff;
855 int rc = iom_list_service(iom_unit_idx, chan, ptro, &send, &uff);
856
857 if (rc < 0)
858 {
859 p->stati = 05001;
860 (void)sir_warn("%s:%d list service failed", __func__, __LINE__);
861
862 return IOM_CMD_ERROR;
863 }
864
865 if (uff)
866 {
867 (void)sir_warn("%s:%d ignoring uff", __func__, __LINE__);
868 }
869
870 if (!send)
871 {
872 (void)sir_warn("%s:%d nothing to send", __func__, __LINE__);
873 p->stati = 05001;
874
875 return IOM_CMD_ERROR;
876 }
877
878 if (IS_IDCW(p) || IS_TDCW(p))
879 {
880 (void)sir_warn("%s:%d expected DDCW", __func__, __LINE__);
881 p->stati = 05001;
882
883 return IOM_CMD_ERROR;
884 }
885
886 *tally = p->DDCW_TALLY;
887
888 if (*tally == 0)
889 {
890 sim_debug(DBG_DEBUG, &mgp_dev,
891 "%s:%d: Tally of zero interpreted as 010000(4096)\r\n", __func__, __LINE__);
892 *tally = 4096;
893 }
894
895 sim_debug(DBG_DEBUG, &mgp_dev,
896 "%s:%d: Tally %d (%o)\r\n", __func__, __LINE__, *tally, *tally);
897
898 if (expected_tally && *tally != expected_tally)
899 {
900 (void)sir_warn("mgp_dev call expected tally of %d; got %d",
901 expected_tally, *tally);
902 p->stati = 05001;
903
904 return IOM_CMD_ERROR;
905 }
906
907 return IOM_CMD_PROCEED;
908 }
909
910 static char *
911 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)
*/
912 {
913 switch (code)
914 {
915 case 000:
916 return "Request status";
917
918 case 001:
919 return "Read";
920
921 case 011:
922 return "Write";
923
924 case 020:
925 return "Host switch down";
926
927 case 040:
928 return "Reset status";
929
930 case 042:
931 return "Disable Bus Back";
932
933 case 043:
934 return "Enable Bus Back";
935
936 case 060:
937 return "Host switch up";
938
939 default:
940 return "Unknown";
941 }
942 }
943
944 # if defined(TESTING)
945 /*
946 * dumppkt: Debug dump of a 36-bit MGP packet.
947 * Kept from the original for debugging purposes.
948 */
949 static void
950 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)
*/
951 {
952 int i;
953 if (words < MGP_PACKET_HEADER_SIZE)
954 {
955 (void)sir_notice("%s: packet too small (%d words)", hdr, words);
956 return;
957 }
958
959 int checksum = getbits36_9(buf[0], 0);
960 int id = getbits36_9(buf[0], 9);
961 int pktype = getbits36_9(buf[0], 18);
962 int flags = getbits36_9(buf[0], 27);
963 int framenr = getbits36_9(buf[1], 0);
964 int rcpt = getbits36_9(buf[1], 9);
965 int pknr = getbits36_9(buf[1], 18);
966 int acknr = getbits36_9(buf[1], 27);
967 int bytecount = (getbits36_9(buf[2], 0) & 0xff)
968 | ((getbits36_9(buf[2], 9) & 0xff) << 8);
969 int srcprc = (getbits36_9(buf[2], 18) & 0xff)
970 | ((getbits36_9(buf[2], 27) & 0xff) << 8);
971 int dstprc = (getbits36_9(buf[3], 0) & 0xff)
972 | ((getbits36_9(buf[3], 9) & 0xff) << 8);
973 int chopcode = getbits36_9(buf[3], 18);
974
975 (void)sir_notice("%s packet (%d words)",
976 hdr, words);
977 (void)sir_notice("cks %#x, id %#x, type %#x, flags %#x",
978 checksum, id, pktype, flags);
979 (void)sir_notice("frame %#x, rcpt %#x, pknr %#x, acknr %#x",
980 framenr, rcpt, pknr, acknr);
981 (void)sir_notice("bytecount %d, src %#x, dst %#x, chopcode %#o",
982 bytecount, srcprc, dstprc, chopcode);
983
984 int pklen = MGP_PACKET_HEADER_SIZE + (bytecount / 4)
985 + (bytecount % 4 ? 1 : 0);
986 if (pklen > (int)words)
987 {
988 pklen = (int)words;
989 }
990
991 for (i = 0; i < pklen; i++)
992 {
993 int lh = getbits36_18(buf[i], 0);
994 int rh = getbits36_18(buf[i], 18);
995 int b0 = getbits36_9 (buf[i], 0);
996 int b1 = getbits36_9 (buf[i], 9);
997 int b2 = getbits36_9 (buf[i], 18);
998 int b3 = getbits36_9 (buf[i], 27);
999 (void)sir_notice(" %d: %06o,,%06o = 0x%02x %02x %02x %02x",
1000 i, lh, rh, b0, b1, b2, b3);
1001 }
1002
1003 (void)sir_notice("EOP");
1004 }
1005 # endif
1006
1007 /* Forward declarations - defined later in this file */
1008 static int mgp_check_dma_ptw(uint iom_unit_idx, uint chan, uint max_words);
1009 static void mgp_validate_dcw_state(uint iom_unit_idx, uint chan, int expected_tally,
1010 const char *caller);
1011
1012 /*
1013 * mgp_validate_dcw_state() - Diagnostic: validate DCW list state vs Multics memory.
1014 *
1015 * Called when an anomalous DDCW_TALLY or DDCW_ADDR is detected. Logs:
1016 * 1. Current iom_chan_data fields (cached values the IOM is using).
1017 * 2. The raw DCW list entries from Multics memory (workspace page 0),
1018 * so we can see whether the corruption is in the cache or in memory.
1019 *
1020 * The workspace structure (from mgp_read_dcm_.pl1 declare read_workspace):
1021 * offset 0-11: dcw_list[0..5] - 6 x (idcw + ddcw) = 12 words
1022 * offset 12: tdcw - 1 word (transfer/wrap-around DCW)
1023 * offset 13: reset_idcw - 1 word
1024 * offset 14-xx: status_queue[0..3] - 4 x istat (size varies)
1025 * offset ~22: buffer[0..5] - 6 x data_size words (129 read, 128 write)
1026 *
1027 * Expected DDCW_ADDR values (for the 6 buffer slots) should be within
1028 * [0, 01777] octal (the workspace fits in one IOM page = 1024 words).
1029 * Expected DDCW_TALLY is 129 (read channel) or 128 (write channel).
1030 *
1031 * If DDCW_ADDR or DDCW_TALLY is outside those bounds, DCW corruption has
1032 * occurred - most likely from a prior PTW-failure DMA write to address ~= 0
1033 * that overwrote the IOM mailbox and caused iom_list_service to follow a
1034 * bad LPW pointer into arbitrary memory.
1035 */
1036 static void
1037 mgp_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)
*/
1038 const char *caller)
1039 {
1040 iom_chan_data_t *p = &iom_chan_data[iom_unit_idx][chan];
1041
1042 /* 1. Dump the cached iom_chan_data state */
1043 (void)sir_notice("DCW_VALIDATE [%s] chan=%d:",
1044 caller, chan);
1045 (void)sir_notice(" cached: DDCW_ADDR=0%o DDCW_TALLY=%d (expected ~%d)",
1046 p->DDCW_ADDR, (int)p->DDCW_TALLY, expected_tally);
1047 (void)sir_notice(" DDCW_22_23_TYPE=%d",
1048 (int)p->DDCW_22_23_TYPE);
1049 (void)sir_notice(" LPW_DCW_PTR=0%o LPW_TALLY=%d",
1050 p->LPW_DCW_PTR, (int)p->LPW_TALLY);
1051 (void)sir_notice(" PCW_PAGE_TABLE_PTR=0%o PCW_63_PTP=%d PCW_64_PGE=%d SEG=%d",
1052 p->PCW_PAGE_TABLE_PTR, (int)p->PCW_63_PTP, (int)p->PCW_64_PGE, (int)p->SEG);
1053 (void)sir_notice(" in_use=%d masked=%d",
1054 (int)p->in_use, (int)p->masked);
1055
1056 /* 2. Sanity-check DDCW_ADDR range (workspace = one IOM page = 1024 words) */
1057 if (p->DDCW_ADDR > 01777)
1058 {
1059 (void)sir_warn("DCW_VALIDATE: DDCW_ADDR=0%o is OUTSIDE workspace range"
1060 " [0, 01777] - LPW likely corrupted", p->DDCW_ADDR);
1061 }
1062
1063 /* 3. Read DCW list from Multics memory in paged mode */
1064 if (!p->PCW_63_PTP || !p->PCW_64_PGE)
1065 {
1066 (void)sir_warn("DCW_VALIDATE: not in paged mode"
1067 " (PTP=%d PGE=%d) - skipping memory read",
1068 (int)p->PCW_63_PTP, (int)p->PCW_64_PGE);
1069 return;
1070 }
1071
1072 /* Look up the workspace page 0 PTW */
1073 word24 pgte0 = (((word24)(p->PCW_PAGE_TABLE_PTR & MASK18)) << 6)
1074 + (((word24)(p->SEG & 1)) << 8)
1075 + 0u; /* page 0 */
1076
1077 word36 ptw0 = 0;
1078 iom_core_read(iom_unit_idx, pgte0, &ptw0, __func__);
1079
1080 int ptw0_valid = ((ptw0 & 0740000777747llu) == 04llu);
1081 (void)sir_warn("DCW_VALIDATE: workspace page 0 PTW at pgte=0%o:"
1082 " 0%012llo (%s)",
1083 pgte0, (unsigned long long)ptw0,
1084 ptw0_valid ? "valid" : "INVALID");
1085
1086 if (!ptw0_valid)
1087 {
1088 (void)sir_warn("DCW_VALIDATE: page 0 PTW invalid -"
1089 " cannot read DCW list from Multics memory");
1090 return;
1091 }
1092
1093 /* Physical base address of workspace (bits 4-17 of PTW, shifted left 10) */
1094 word24 phys_base = ((word24)((ptw0 >> 18) & MASK14)) << 10;
1095 (void)sir_warn("DCW_VALIDATE: workspace physical base = 0%o", phys_base);
1096
1097 /* 4. Read and validate each of the 6 IDCW+DDCW pairs.
1098 * The dcw_list occupies offsets 0-11 in the workspace:
1099 * slot i: IDCW at offset 2*i, DDCW at offset 2*i+1 */
1100 (void)sir_warn("DCW_VALIDATE: dcw_list entries from memory"
1101 " (expected TALLY=%d, ADDR in [0,01777]):", expected_tally);
1102 for (int i = 0; i < 6; i++)
1103 {
1104 word24 idcw_phys = phys_base + (word24)(2 * i);
1105 word24 ddcw_phys = phys_base + (word24)(2 * i + 1);
1106
1107 word36 idcw_word = 0, ddcw_word = 0;
1108 iom_core_read(iom_unit_idx, idcw_phys, &idcw_word, __func__);
1109 iom_core_read(iom_unit_idx, ddcw_phys, &ddcw_word, __func__);
1110
1111 /* DDCW layout: bits 0-17 = address, bits 24-35 = tally */
1112 uint ddcw_addr = (uint)((ddcw_word >> 18) & MASK18);
1113 uint ddcw_tally = (uint)(ddcw_word & 0xFFFu);
1114
1115 int anomalous = (ddcw_tally == 0
1116 || (int)ddcw_tally > (expected_tally + 2)
1117 || ddcw_addr > 01777u);
1118
1119 (void)sir_warn(" slot[%d]: IDCW=0%012llo DDCW=0%012llo"
1120 " addr=0%o tally=%d%s",
1121 i,
1122 (unsigned long long)idcw_word,
1123 (unsigned long long)ddcw_word,
1124 ddcw_addr, ddcw_tally,
1125 anomalous ? " *** ANOMALOUS" : "");
1126 }
1127
1128 /* 5. Read the word at LPW_DCW_PTR to see what iom_list_service last fetched */
1129 word18 lpw_ptr = p->LPW_DCW_PTR;
1130 if (lpw_ptr <= 01777u)
1131 {
1132 word36 lpw_word = 0;
1133 iom_core_read(iom_unit_idx, phys_base + (word24)lpw_ptr, &lpw_word, __func__);
1134 (void)sir_warn("DCW_VALIDATE: mem[LPW_DCW_PTR=0%o] = 0%012llo",
1135 lpw_ptr, (unsigned long long)lpw_word);
1136 }
1137 else
1138 {
1139 (void)sir_warn("DCW_VALIDATE: LPW_DCW_PTR=0%o is OUTSIDE workspace"
1140 " [0, 01777] - LPW pointer corrupted", lpw_ptr);
1141 }
1142 }
1143
1144 static iom_cmd_rc_t
1145 mgp_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)
*/
1146 {
1147 # if defined(TESTING)
1148 cpu_state_t * cpup = _cpup;
1149 # endif
1150 iom_chan_data_t *p = &iom_chan_data[iom_unit_idx][chan];
1151
1152 sim_debug(DBG_TRACE, &mgp_dev,
1153 "mgp_cmd CHAN_CMD %o DEV_CODE %o DEV_CMD %o COUNT %o\r\n",
1154 p->IDCW_CHAN_CMD, p->IDCW_DEV_CODE, p->IDCW_DEV_CMD, p->IDCW_COUNT);
1155
1156 // Not IDCW?
1157 if (IS_NOT_IDCW(p))
1158 {
1159 (void)sir_warn("%s:%d: Unexpected IOTx", __func__, __LINE__);
1160
1161 return IOM_CMD_ERROR;
1162 }
1163
1164 bool ptro;
1165
1166 sim_debug(DBG_DEBUG, &mgp_dev, "mgp_cmd %#o (%s)\r\n",
1167 p->IDCW_DEV_CMD, cmd_name(p->IDCW_DEV_CMD));
1168
1169 switch (p->IDCW_DEV_CMD)
1170 {
1171 case 000: // CMD 00 Request status
1172 {
1173 p->stati = 04000;
1174 sim_debug(DBG_DEBUG, &mgp_dev, "mgp request status\r\n");
1175 }
1176 break;
1177
1178 case 001: // CMD 01 Read
1179 {
1180 sim_debug(DBG_DEBUG, &mgp_dev, "%s:%d: mgp_dev_$read\r\n", __func__, __LINE__);
1181
1182 const uint expected_tally = 0;
1183 uint tally;
1184 iom_cmd_rc_t rc
1185 = get_ddcw(p, iom_unit_idx, chan, &ptro, expected_tally, &tally);
1186 if (rc)
1187 {
1188 return rc;
1189 }
1190
1191 /* Validate the DMA PTW before any IOM buffer access.
1192 * If Multics's memory manager has paged out the IOM buffer (e.g. after
1193 * extended idle), iom_indirect_data_service() would compute physical
1194 * address ~= 0 from the zero PTW and either read garbage or corrupt the
1195 * IOM mailbox area (addresses 0-0777). Return IOM_CMD_DISCONNECT so the
1196 * IOM sends a terminate interrupt; Multics can then re-establish the
1197 * channel with properly pinned memory.
1198 *
1199 * max_words=256: NCP packets are at most MAX_PKT_BYTES=504 bytes =
1200 * ~126 36-bit words, which fits within the first IOM page (1024 words).
1201 * Using max_words=0 (full DDCW_TALLY=4096 words = 4 pages) would cause
1202 * false positives when Multics's memory manager pages out unused pages
1203 * 1-3 of the large DCW buffer overnight - those pages are never touched
1204 * by the DMA since the actual payload is only ~126 words. */
1205 if (!mgp_check_dma_ptw(iom_unit_idx, chan, 256))
1206 {
1207 (void)sir_warn("%s:%d: CMD 001 invalid DMA PTW on chan %d - "
1208 "skipping buffer access, sending terminate interrupt",
1209 __func__, __LINE__, chan);
1210 mgp_dev_state.ptw_failed_at = time(NULL);
1211 p->stati = 04000;
1212 return IOM_CMD_DISCONNECT;
1213 }
1214
1215 /* Sanity-check DDCW_TALLY.
1216 *
1217 * Legitimate MGP read DDCWs have TALLY set by initialize_workspace
1218 * in mgp_read_dcm_.pl1: data_size=129 words. TALLY=0 (IOM convention
1219 * for 4096) or an implausibly large value means the DCW entry was
1220 * corrupted. The primary corruption path (TDCW wrap -> DDCW_ADDR=0 ->
1221 * IDS overwrites DCW list) is now blocked by the DDCW_ADDR range check
1222 * in mgp_process_event; this check is retained as defense-in-depth. */
1223 if (p->DDCW_TALLY == 0 || p->DDCW_TALLY > MGP_MAX_TALLY)
1224 {
1225 (void)sir_warn("%s:%d: CMD 001 implausible DDCW_TALLY=%d on chan %d"
1226 " (expected ~129, max %d - DCW corruption?)"
1227 " sending terminate interrupt",
1228 __func__, __LINE__, p->DDCW_TALLY, chan, MGP_MAX_TALLY);
1229 mgp_validate_dcw_state(iom_unit_idx, chan, 129, "CMD001-TALLY");
1230 p->stati = 04000;
1231 return IOM_CMD_DISCONNECT;
1232 }
1233
1234 /* Read current buffer and complete DDCW processing */
1235 word36 buffer[256];
1236 uint words_processed;
1237 iom_indirect_data_service(
1238 iom_unit_idx, chan, buffer, &words_processed, false);
1239
1240 sim_debug(DBG_DEBUG, &mgp_dev,
1241 "%s:%d: Read unit %#x chan %#x (%d), %d words\r\n",
1242 __func__, __LINE__, iom_unit_idx, chan, chan, words_processed);
1243
1244 /*
1245 * Mark that Multics wants to read. The actual data delivery happens
1246 * in mgp_process_event() when the NCP sends us a packet.
1247 */
1248 mgp_dev_state.want_to_read = 1;
1249 mgp_dev_state.want_to_read_since = time(NULL);
1250 mgp_dev_state.read_unit_idx = iom_unit_idx;
1251 mgp_dev_state.read_unit_chan = chan;
1252
1253 /* Write back to IOM to complete the DDCW processing.
1254 * This is required before returning IOM_CMD_PENDING -
1255 * without it the IOM channel state is inconsistent and
1256 * Multics will timeout and mask the channel.
1257 * (cf. new_dps8m_chaos_code/dps8_mgp.c.new lines 525-526)
1258 */
1259 iom_indirect_data_service(
1260 iom_unit_idx, chan, buffer, &words_processed, true);
1261
1262 p->stati = 04000;
1263 /* Signal mgp_process_event that this delivery attempt succeeded
1264 * (iom_continue_channel successfully called mgp_cmd). */
1265 mgp_dev_state.delivery_succeeded = 1;
1266 return IOM_CMD_PENDING;
1267 }
1268 /*NOTREACHED*/ /* unreachable */
1269 break;
1270
1271 case 011: // CMD 11 Write
1272 {
1273 sim_debug(DBG_DEBUG, &mgp_dev, "%s:%d: mgp_dev_$write\r\n", __func__, __LINE__);
1274
1275 const uint expected_tally = 0;
1276 uint tally;
1277 iom_cmd_rc_t rc
1278 = get_ddcw(p, iom_unit_idx, chan, &ptro, expected_tally, &tally);
1279
1280 /* Check get_ddcw result (same as CMD 001 - missing this check was a
1281 * bug: a failed get_ddcw left p->DDCW_ADDR stale, causing the PTW
1282 * check below to validate the wrong page).
1283 *
1284 * NOTE: get_ddcw() returns IOM_CMD_PROCEED (0) on success or
1285 * IOM_CMD_ERROR (-1) on failure - never IOM_CMD_PENDING.
1286 * Returning IOM_CMD_ERROR here sends a TERMINATE interrupt (not marker)
1287 * via the rc<0 path in doPayloadChannel/iom_continue_channel. Multics
1288 * sees the terminate and re-issues the write; the packet content is
1289 * lost (silent frame drop -> "Unordered frame" in NCP log).
1290 * Logged as WARNING so we can correlate with NCP-side drops. */
1291 if (rc)
1292 {
1293 (void)sir_warn("%s:%d: CMD 011 get_ddcw failed rc=%d on chan %d"
1294 " - packet will be silently dropped (frame loss)",
1295 __func__, __LINE__, rc, chan);
1296 return rc;
1297 }
1298
1299 /* Validate PTW before DMA access (prevents mailbox corruption if
1300 * Multics has paged out the WRITE channel buffer).
1301 * max_words=256: same rationale as CMD 001 - the write payload is at
1302 * most ~126 36-bit words (MAX_PKT_BYTES / 4), which fits within the
1303 * current IOM page. Checking the full DDCW_TALLY (4096 words = 4
1304 * pages) would cause false positives when pages beyond the payload
1305 * are paged out, leading to tight IOM_CMD_DISCONNECT loops. */
1306 if (!mgp_check_dma_ptw(iom_unit_idx, chan, 256))
1307 {
1308 (void)sir_warn("%s:%d: CMD 011 invalid DMA PTW on chan %d - "
1309 "skipping buffer access, sending terminate interrupt",
1310 __func__, __LINE__, chan);
1311 mgp_dev_state.ptw_failed_at = time(NULL);
1312 if (mgp_dev_state.pipe)
1313 {
1314 uv_close((uv_handle_t *)mgp_dev_state.pipe, ncp_close_cb);
1315 mgp_dev_state.pipe = NULL;
1316 } /* discard queued NOOPs */
1317 p->stati = 04000;
1318 return IOM_CMD_DISCONNECT;
1319 }
1320
1321 /* Sanity-check DDCW_TALLY (same rationale as CMD 001).
1322 * The write channel uses data_size=128 (mgp_write_dcm_.pl1).
1323 * iom_indirect_data_service's READ path (first call below, reading
1324 * the outgoing packet from Multics memory) ignores cnt and walks
1325 * all p->DDCW_TALLY words; TALLY=0 -> 4096-word walk -> same console-
1326 * flooding cascade as CMD 001. */
1327 if (p->DDCW_TALLY == 0 || p->DDCW_TALLY > MGP_MAX_TALLY)
1328 {
1329 (void)sir_warn("%s:%d CMD 011 implausible DDCW_TALLY=%d on chan %d"
1330 " (expected ~128, max %d - DCW corruption?)"
1331 " sending terminate interrupt",
1332 __func__, __LINE__, p->DDCW_TALLY, chan, MGP_MAX_TALLY);
1333 mgp_validate_dcw_state(iom_unit_idx, chan, 128, "CMD011-TALLY");
1334 mgp_dev_state.ptw_failed_at = time(NULL);
1335 if (mgp_dev_state.pipe)
1336 {
1337 uv_close((uv_handle_t *)mgp_dev_state.pipe, ncp_close_cb);
1338 mgp_dev_state.pipe = NULL;
1339 }
1340 p->stati = 04000;
1341 return IOM_CMD_DISCONNECT;
1342 }
1343
1344 word36 buffer[256];
1345 uint words_processed;
1346 iom_indirect_data_service(
1347 iom_unit_idx, chan, buffer, &words_processed, false);
1348
1349 sim_debug(DBG_DEBUG, &mgp_dev,
1350 "%s:%d: Write unit %#x chan %#x (%d), %d words\r\n",
1351 __func__, __LINE__, iom_unit_idx, chan, chan, words_processed);
1352 # if defined(TESTING)
1353 if (sim_deb && (mgp_dev.dctrl & DBG_DEBUG))
1354 {
1355 dumppkt("Write", buffer, words_processed);
1356 }
1357 # endif
1358 /* Convert 36-bit words to 8-bit bytes */
1359 u_char pkt8[MAX_PKT_BYTES];
1360 int pktlen = word36_to_pkt8(buffer, words_processed, pkt8, MAX_PKT_BYTES);
1361
1362 /* Send to NCP */
1363 int v = ncp_send_packet(pkt8, pktlen);
1364 if (v < 0)
1365 {
1366 (void)sir_warn("%s:%d: ncp_send_packet failed", __func__, __LINE__);
1367 }
1368
1369 /* Return value depends on IDCW control field:
1370 *
1371 * mgp_write_dcm_ uses a double-buffer (half_buffer_size=3 slots per
1372 * half). When fill>=2, update_workspace_and_start_io patches the first
1373 * IDCW to CHAN_CTRL_PROCEED (=2, "no-terminate") and sets the last to
1374 * CHAN_CTRL_TERMINATE (=0). start_io calls ioi_$connect and resets
1375 * fill=0. doPayloadChannel's do-while loop then processes each IDCW:
1376 *
1377 * - IOM_CMD_PROCEED (0): loop continues -> iom_list_service advances
1378 * to the next IDCW -> mgp_cmd(011) called again for the next packet.
1379 * - IOM_CMD_DISCONNECT (2): sets terminate=true -> loop exits after
1380 * this iteration -> terminate interrupt sent.
1381 *
1382 * Without this check, we always returned IOM_CMD_DISCONNECT for every
1383 * IDCW, causing the loop to exit after the FIRST packet in a batch.
1384 * The second packet was orphaned (start_io had already reset fill=0, so
1385 * check_status_queue on the next write call skipped start_io entirely).
1386 * Result: frame N sent, frame N+1 silently dropped -> "Unordered frame"
1387 * in NCP log and truncated file transfer. */
1388 rc = (p->IDCW_CHAN_CTRL == CHAN_CTRL_TERMINATE)
1389 ? IOM_CMD_DISCONNECT /* terminate IDCW: send terminate interrupt */
1390 : IOM_CMD_PROCEED; /* no-terminate IDCW: continue DCW list loop */
1391 p->stati = 04000;
1392
1393 /* Write-back: re-validates PTW before writing back to the Multics
1394 * write DMA buffer. ncp_connect() inside ncp_send_packet() can
1395 * block briefly (Unix connect syscall), during which CPU A (Multics)
1396 * may page out the DMA buffer. If the page is now gone, skip the
1397 * write-back (avoids fetch_IDSPTW warnings and address-0 corruption),
1398 * close the socket to flush queued NOOPs, and let the terminate
1399 * interrupt trigger Multics channel recovery.
1400 * max_words=256: same rationale as initial PTW check above - check
1401 * only the payload area, not unused pages beyond it. */
1402 if (!mgp_check_dma_ptw(iom_unit_idx, chan, 256))
1403 {
1404 (void)sir_warn("%s:%d: CMD 011 PTW invalid after ncp_send_packet on chan %d"
1405 " - skipping write-back", __func__, __LINE__, chan);
1406 mgp_dev_state.ptw_failed_at = time(NULL);
1407 if (mgp_dev_state.pipe)
1408 {
1409 uv_close((uv_handle_t *)mgp_dev_state.pipe, ncp_close_cb);
1410 mgp_dev_state.pipe = NULL;
1411 }
1412 return rc; /* IOM_CMD_DISCONNECT - sends terminate interrupt */
1413 }
1414
1415 iom_indirect_data_service(
1416 iom_unit_idx, chan, buffer, &words_processed, true);
1417
1418 return rc;
1419 }
1420 /*NOTREACHED*/ /* unreachable */
1421 break;
1422
1423 case 006: // CMD 06 (seen during channel restart; acknowledge gracefully)
1424 {
1425 p->stati = 04000;
1426 sim_debug(DBG_DEBUG, &mgp_dev, "mgp cmd 006 (handled)\r\n");
1427 }
1428 break;
1429
1430 case 020: // CMD 20 Host switch down
1431 {
1432 p->stati = 04000;
1433 sim_debug(DBG_DEBUG, &mgp_dev, "mgp host switch down\r\n");
1434 }
1435 break;
1436
1437 case 040: // CMD 40 Reset status
1438 {
1439 p->stati = 04000;
1440 }
1441 break;
1442
1443 case 042: // CMD 42 Disable Bus Back
1444 {
1445 p->stati = 04000;
1446 sim_debug(DBG_DEBUG, &mgp_dev, "mgp disable bus back\r\n");
1447 }
1448 break;
1449
1450 case 043: // CMD 43 Enable Bus Back
1451 {
1452 p->stati = 04000;
1453 sim_debug(DBG_DEBUG, &mgp_dev, "mgp enable bus back\r\n");
1454 }
1455 break;
1456
1457 case 060: // CMD 60 Host switch up
1458 {
1459 p->stati = 04000;
1460 sim_debug(DBG_DEBUG, &mgp_dev, "mgp host switch up\r\n");
1461 }
1462 break;
1463
1464 default:
1465 {
1466 if (p->IDCW_DEV_CMD != 051) // ignore bootload console probe
1467 {
1468 (void)sir_warn("%s:%d: MGP unrecognized device command %02o",
1469 __func__, __LINE__, p->IDCW_DEV_CMD);
1470 }
1471
1472 p->stati = 04501; // cmd reject, invalid opcode
1473 p->chanStatus = chanStatIncorrectDCW;
1474 }
1475 return IOM_CMD_ERROR;
1476 }
1477
1478 if (p->IDCW_CHAN_CMD == 0)
1479 {
1480 return IOM_CMD_DISCONNECT; // don't do DCW list
1481 }
1482
1483 return IOM_CMD_PROCEED;
1484 }
1485
1486 iom_cmd_rc_t
1487 mgp_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)
*/
1488 {
1489 iom_chan_data_t *p = &iom_chan_data[iom_unit_idx][chan];
1490
1491 // Is it an IDCW?
1492 if (IS_IDCW(p))
1493 {
1494 return mgp_cmd(iom_unit_idx, chan);
1495 }
1496
1497 // T&D: the executive's channel survey sends data DCWs to every
1498 // channel; not worth a notice there
1499 if (! tandd_mode_any ())
1500 (void)sir_notice("%s:%d: expected IDCW", __func__, __LINE__);
1501
1502 return IOM_CMD_ERROR;
1503 }
1504
1505 /*
1506 * mgp_check_dma_ptw() - Validate that ALL pages of the IOM DMA buffer for
1507 * channel `chan` have valid page table words (PTWs) before calling
1508 * iom_indirect_data_service().
1509 *
1510 * ioi_$workspace pins all workspace pages (DCW list and data buffers) in
1511 * physical memory for the duration of active I/O (while in_use=true).
1512 * Page eviction cannot occur. However, if the DCW list becomes corrupted
1513 * (e.g., circular-buffer wrap during an RCP force-detach/reattach cycle),
1514 * DDCW_ADDR may point outside the wired workspace, where no PTW exists.
1515 * Calling iom_indirect_data_service() with PTW=0 computes physical address
1516 * ~= 0, overwriting the IOM mailbox area (addresses 0-0777). That corruption
1517 * cascades: Multics's IOM interrupt handler reads garbage vectors ->
1518 * fault/interrupt storm -> 100% CPU and system hang.
1519 *
1520 * The buffer spans from DDCW_ADDR through DDCW_ADDR+tally-1, potentially
1521 * crossing page boundaries. Only checking the first page misses unmapped
1522 * pages later in the buffer (symptom: fetch_IDSPTW warnings at addr 0o02000+
1523 * after a force-detach/reattach cycle). This function validates ALL pages
1524 * in the range so that any zero PTW is caught before the DMA starts.
1525 *
1526 * This function replicates the PTW lookup from fetch_IDSPTW /
1527 * build_IDSPTW_address (both static in dps8_iom.c) so dps8_mgp.c can
1528 * pre-check validity without touching the IOM code.
1529 *
1530 * Returns: 1 if all PTWs in the buffer range are valid (safe to proceed),
1531 * 0 if any PTW is zero or otherwise invalid (skip the DMA).
1532 */
1533 static int
1534 mgp_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)
*/
1535 {
1536 iom_chan_data_t * p = & iom_chan_data[iom_unit_idx][chan];
1537
1538 /* If the channel is not in paged mode, no PTW to validate. */
1539 if (!p->PCW_63_PTP || !p->PCW_64_PGE)
1540 return 1;
1541
1542 /* Determine the range of IOM pages the buffer spans.
1543 * tally=0 is interpreted as 4096 by get_ddcw / iom_indirect_data_service.
1544 * page numbers are 8-bit (IOM page table has at most 256 entries).
1545 *
1546 * max_words: when non-zero, caps the effective tally used for page range
1547 * calculation. Use this when the caller knows it will only write a small
1548 * payload (e.g. mgp_process_event delivers NCP packets of at most ~128
1549 * 36-bit words) so that pages beyond the actual payload are not validated
1550 * unnecessarily. Pass 0 to use the full DDCW_TALLY (or 4096). */
1551 uint raw_tally = p->DDCW_TALLY ? p->DDCW_TALLY : 4096;
1552 uint tally = (max_words && max_words < raw_tally) ? max_words : raw_tally;
1553 word18 start_page = (p->DDCW_ADDR >> 10) & MASK8;
1554 word18 end_page = ((p->DDCW_ADDR + tally - 1) >> 10) & MASK8;
1555
1556 /* Replicate build_IDSPTW_address() from dps8_iom.c for each page:
1557 * pgte = ((PCW_PAGE_TABLE_PTR & MASK18) << 6)
1558 * + ((SEG & 1) << 8)
1559 * + (pageNumber & MASK8) */
1560 for (word18 page = start_page; page <= end_page; page++)
1561 {
1562 word24 pgte = (((word24)(p->PCW_PAGE_TABLE_PTR & MASK18)) << 6)
1563 + (((word24)(p->SEG & 1)) << 8)
1564 + (page & MASK8);
1565
1566 word36 ptw;
1567 iom_core_read(iom_unit_idx, pgte, &ptw, __func__);
1568
1569 /* Valid PTW has specific bits set; zero PTW means page not present. */
1570 if ((ptw & 0740000777747llu) != 04llu)
1571 {
1572 (void)sir_warn ("%s:%d: chan %d DDCW_ADDR 0%o page %u/%u: invalid PTW"
1573 " 0%012llo at pgte 0%o"
1574 " (PCW_PAGE_TABLE_PTR=0%o SEG=%d tally=%u)",
1575 __func__, __LINE__, chan, p->DDCW_ADDR,
1576 (unsigned)(page - start_page + 1),
1577 (unsigned)(end_page - start_page + 1),
1578 (unsigned long long)ptw, pgte,
1579 p->PCW_PAGE_TABLE_PTR, (int)p->SEG, tally);
1580 return 0;
1581 }
1582 }
1583 return 1;
1584 }
1585
1586 /*
1587 * mgp_read_ring_state() - Would filling the current buffer fill the ring?
1588 *
1589 * mgp_read_dcm_'s start_io stores a terminating IDCW at buffer_number, the
1590 * next buffer it will read, and puts the old one back to non-terminating.
1591 * The read channel is pointing at buffer N (IDCW[N] and DDCW[N] already
1592 * fetched, LPW at the word after DDCW[N]). If the IDCW after it in the
1593 * list, IDCW[N+1] (via the TDCW for the last buffer), is the terminating
1594 * one, Multics has not yet read buffer N+1, and filling buffer N would make
1595 * every buffer unread: the marker for it gives stop_buffer_number = N+1 =
1596 * buffer_number, which mgp_read_dcm_ takes as "nothing new", and the next
1597 * delivery overwrites an unread buffer. So the packet waits (in in_buffer
1598 * and the NCP socket) until Multics has read on and moved the halt, or
1599 * until the no-progress watchdog resets the channel, which loses it.
1600 *
1601 * The IDCW is read from memory each time, not from the channel state:
1602 * IDCW[N] was fetched before Multics last moved the halt, so its cached
1603 * control is stale. When Multics keeps up the halt sits at buffer N
1604 * itself (behind the next IDCW), so this never holds anything up.
1605 *
1606 * The next DCW is found by iom_peek_next_dcw, which follows the TDCW
1607 * after the last buffer as the IOM's list service does (new address and
1608 * the channel mode after the transfer) without changing channel state.
1609 * mgp_read_dcm_'s TDCW has only an address and the type (string = ""b,
1610 * type = "10"b): no SEG/PDTA/PDCW/RES/REL bits, so none of
1611 * iom_peek_next_dcw's refusals apply to it. The mode change it causes
1612 * (e.g. cm4 -> cm5) is modelled; in the paged modes cm3a/cm4/cm5 the list
1613 * is addressed through the same LPW page table before and after it.
1614 *
1615 * RING_UNKNOWN: there is no next IDCW to look at: iom_peek_next_dcw
1616 * failed (invalid PTW, a list mode or TDCW it can't model, two TDCWs in a
1617 * row, RES violations; bad_word = 0), or the next DCW is a data DCW, not
1618 * an IDCW (a damaged list; bad_word = that word). Then it is not known
1619 * whether the next buffer is free, so the caller must not deliver; it
1620 * resets the channel instead (see mgp_reset_read_channel).
1621 */
1622 enum mgp_ring_state { RING_NOT_FULL, RING_FULL, RING_UNKNOWN };
1623
1624 static enum mgp_ring_state
1625 mgp_read_ring_state(uint iom_unit_idx, uint chan, word36 * bad_word)
/* ![[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)
*/
1626 {
1627 word36 w;
1628
1629 * bad_word = 0;
1630 if (iom_peek_next_dcw(iom_unit_idx, chan, &w) != 0)
1631 return RING_UNKNOWN;
1632 if (getbits36_3(w, 18) != 07) /* not an IDCW */
1633 {
1634 * bad_word = w;
1635 return RING_UNKNOWN;
1636 }
1637 /* IDCW: bits 22-23 are the control */
1638 return getbits36_2(w, 22) == CHAN_CTRL_TERMINATE ? RING_FULL : RING_NOT_FULL;
1639 }
1640
1641 /*
1642 * mgp_reset_read_channel() - Give up on the read channel's current I/O.
1643 *
1644 * Used when the DMA target's PTW is invalid and when the next list word
1645 * can't be checked. Close the NCP socket (from the receiver side, close()
1646 * discards the unread packets in the kernel buffer, which would otherwise
1647 * keep triggering failed deliveries; the NCP reconnects and NAK recovery
1648 * re-synchronizes the frame counters), start the reconnect backoff, and
1649 * send one terminate interrupt so Multics re-establishes the channel.
1650 */
1651 static void
1652 mgp_reset_read_channel(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)
*/
1653 {
1654 mgp_dev_state.want_to_read = 0;
1655 mgp_dev_state.ptw_failed_at = time(NULL); /* start reconnect backoff */
1656 if (mgp_dev_state.pipe != NULL)
1657 {
1658 uv_close((uv_handle_t *)mgp_dev_state.pipe, ncp_close_cb);
1659 mgp_dev_state.pipe = NULL;
1660 }
1661 send_terminate_interrupt(mgp_dev_state.read_unit_idx,
1662 mgp_dev_state.read_unit_chan);
1663 }
1664
1665 /*
1666 * mgp_process_event() - Called periodically from the emulator's event loop.
1667 *
1668 * If Multics has a pending read (want_to_read), poll the NCP socket for
1669 * incoming data. If data is available, read it, convert from 8-bit to
1670 * 36-bit words, write to the current IOM workspace buffer[N] via
1671 * iom_indirect_data_service, then call iom_continue_channel() to:
1672 *
1673 * 1. Advance DDCW_ADDR to workspace buffer[N+1] (via one loop iteration
1674 * of doPayloadChannel: fetch IDCW[N+1] -> call mgp_cmd(read) ->
1675 * get_ddcw() sets DDCW_ADDR = buffer[N+1]).
1676 *
1677 * 2. Send a marker interrupt so mgp_read_dcm_'s wakeup handler fires.
1678 * The interrupt's stat.offset causes stop_buffer_number = N+1, which
1679 * is one ahead of the DCM's buffer_number (N), triggering the
1680 * processing loop to consume buffer[N].
1681 *
1682 * Because the interrupt is a MARKER (not terminate), start_io in the DCM
1683 * sees running=true and does NOT call ioi_$connect. The channel remains
1684 * pending with DDCW_ADDR pointing to buffer[N+1], ready for the next
1685 * incoming packet. want_to_read stays set (mgp_cmd restores it inside
1686 * iom_continue_channel).
1687 *
1688 * TDCW wrap-around is handled transparently by iom_list_service inside
1689 * iom_continue_channel.
1690 */
1691 void
1692 mgp_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)
*/
1693 {
1694 ncp_check_eof_drain();
1695 /* Unconditional no-progress watchdog. Runs regardless of want_to_read so
1696 * it cannot be starved by the masked/unmask flicker described above.
1697 * Lazily initializes last_progress_time on first observation so a fresh
1698 * connection (or one that just recovered) gets a full timeout window
1699 * before this can fire. Gated on want_to_read_since > 0: read_unit_idx/
1700 * read_unit_chan are only meaningfully set once Multics has issued at
1701 * least one real read (mgp_cmd CMD 001); before that they are still
1702 * zero-initialized, and firing the watchdog would send a terminate
1703 * interrupt to the wrong (unit 0, chan 0) channel. */
1704 # if defined(TESTING)
1705 cpu_state_t * cpup = _cpup;
1706 # endif
1707 if (mgp_dev_state.pipe != NULL && mgp_dev_state.want_to_read_since > 0)
1708 {
1709 time_t now_np = time(NULL);
1710 if (mgp_dev_state.last_progress_time == 0)
1711 {
1712 mgp_dev_state.last_progress_time = now_np;
1713 }
1714 else if (now_np - mgp_dev_state.last_progress_time >= NO_PROGRESS_TIMEOUT_SECS)
1715 {
1716 (void)sir_warn("%s:%d: no packet delivered to Multics in %d+ seconds despite "
1717 "NCP connection; forcing socket reset and terminate "
1718 "interrupt to break a possible masked/unmask timeout loop",
1719 __func__, __LINE__, NO_PROGRESS_TIMEOUT_SECS);
1720 if (mgp_dev_state.pipe)
1721 {
1722 uv_close((uv_handle_t *)mgp_dev_state.pipe, ncp_close_cb);
1723 mgp_dev_state.pipe = NULL;
1724 }
1725 mgp_dev_state.last_progress_time = 0;
1726 mgp_dev_state.masked_since = 0;
1727 mgp_dev_state.want_to_read = 0;
1728 send_terminate_interrupt(mgp_dev_state.read_unit_idx,
1729 mgp_dev_state.read_unit_chan);
1730 return;
1731 }
1732 }
1733
1734 if (!mgp_dev_state.want_to_read)
1735 {
1736 return;
1737 }
1738
1739 uint iom_unit_idx = mgp_dev_state.read_unit_idx;
1740 uint chan = mgp_dev_state.read_unit_chan;
1741 iom_chan_data_t * p = & iom_chan_data[iom_unit_idx][chan];
1742
1743 /* If the channel has been masked (Multics sent a PCW with MSK=1), stop
1744 * trying to deliver packets until Multics re-enables it with a fresh
1745 * Connect PCW (MSK=0). want_to_read is restored by mgp_cmd() when
1746 * Multics issues the new read command inside doPayloadChannel.
1747 *
1748 * NOTE: We do NOT check !in_use here. The IOM sets in_use=false after
1749 * a terminate interrupt (e.g. DCW fault), but iom_continue_channel()
1750 * needs to run in that case so it can advance the DCW list and generate
1751 * the terminate interrupt that tells Multics to re-issue the read command.
1752 * Blocking on !in_use (without masked) prevents that signalling and
1753 * causes Multics to stall for ~30 seconds until its d102 timer fires. */
1754 {
1755 if (p->masked)
1756 {
1757 mgp_dev_state.want_to_read = 0;
1758
1759 /* If the NCP socket is connected, track how long the channel has been
1760 * stuck masked. After MASKED_STUCK_TIMEOUT_SECS, close the socket and
1761 * send a terminate interrupt. This breaks the deadlock:
1762 * - masked channel -> no delivery -> NCP inflight=30 -> no more NOOPs
1763 * - ioi_masked$timer fires but "masked while in use" prevents recovery
1764 * The terminate interrupt tells Multics the I/O failed so it re-issues
1765 * READ with a fresh channel state. The NCP-absent path then handles
1766 * reconnection cleanly. */
1767 if (mgp_dev_state.pipe != NULL)
1768 {
1769 time_t now = time(NULL);
1770 if (mgp_dev_state.masked_since == 0)
1771 {
1772 mgp_dev_state.masked_since = now;
1773 sim_debug(DBG_DEBUG, &mgp_dev,
1774 "%s:%d: channel %d masked while NCP connected; "
1775 "starting stuck timer\r\n", __func__, __LINE__, chan);
1776 }
1777 else if (now - mgp_dev_state.masked_since >= MASKED_STUCK_TIMEOUT_SECS)
1778 {
1779 (void)sir_warn("%s:%d: channel %d masked+stuck for %d+ seconds; "
1780 "closing NCP socket and sending terminate interrupt "
1781 "to force recovery",
1782 __func__, __LINE__, chan, MASKED_STUCK_TIMEOUT_SECS);
1783 if (mgp_dev_state.pipe)
1784 {
1785 uv_close((uv_handle_t *)mgp_dev_state.pipe, ncp_close_cb);
1786 mgp_dev_state.pipe = NULL;
1787 }
1788 mgp_dev_state.masked_since = 0;
1789 send_terminate_interrupt(mgp_dev_state.read_unit_idx,
1790 mgp_dev_state.read_unit_chan);
1791 }
1792 }
1793 else
1794 {
1795 /* NCP not connected. Reset the masked-stuck timer (it's only
1796 * meaningful when the NCP is connected), but also run the
1797 * NCP-absent timeout so the IOM channel's in_use=true state is
1798 * eventually released.
1799 *
1800 * Without this: masked=true causes us to return here every 10ms,
1801 * bypassing the NCP-absent timeout check below. If the channel
1802 * is masked AND the NCP is absent, in_use never clears, and
1803 * ioi_masked$timer fires every ~4 minutes finding "chan N masked
1804 * while in use" - a permanent stuck deadlock.
1805 *
1806 * With this: after NCP_ABSENT_TIMEOUT_SECS (30 s) we send a
1807 * terminate interrupt. in_use becomes false. The next
1808 * ioi_masked$timer invocation successfully reconnects the masked
1809 * channel (no longer masked+in_use), and normal operation
1810 * resumes once the NCP connects again. */
1811 mgp_dev_state.masked_since = 0;
1812 time_t now_ma = time(NULL);
1813 if (mgp_dev_state.want_to_read_since > 0 &&
1814 now_ma - mgp_dev_state.want_to_read_since >= NCP_ABSENT_TIMEOUT_SECS)
1815 {
1816 sim_debug(DBG_DEBUG, &mgp_dev,
1817 "%s:%d: NCP absent + channel %d masked for %d+ s; "
1818 "sending terminate interrupt to release in_use\r\n",
1819 __func__, __LINE__, chan, NCP_ABSENT_TIMEOUT_SECS);
1820 mgp_dev_state.want_to_read = 0;
1821 send_terminate_interrupt(mgp_dev_state.read_unit_idx,
1822 mgp_dev_state.read_unit_chan);
1823 }
1824 }
1825 return;
1826 }
1827
1828 /* Channel is not masked - clear the stuck timer */
1829 mgp_dev_state.masked_since = 0;
1830 }
1831
1832 /* Periodic diagnostic: log socket state every 60 seconds at debug level */
1833 {
1834 static time_t last_diag = 0;
1835 time_t now = time(NULL);
1836 if (now - last_diag >= 60)
1837 {
1838 sim_debug(DBG_DEBUG, &mgp_dev,
1839 "MGP diag: want_to_read=%d pipe=%p unit=%d chan=%d\r\n",
1840 mgp_dev_state.want_to_read,
1841 mgp_dev_state.pipe,
1842 mgp_dev_state.read_unit_idx,
1843 mgp_dev_state.read_unit_chan);
1844 last_diag = now;
1845 }
1846 }
1847
1848 /* If the NCP is not connected, try to connect first. If still not
1849 * connected after NCP_ABSENT_TIMEOUT_SECS, release the IOM channel via
1850 * a terminate interrupt. Without this release, the channel stays in
1851 * IOM_CMD_PENDING indefinitely; Multics's ioi_masked$timer eventually
1852 * fires and sends a mask PCW while the channel is still "in use",
1853 * producing spurious "doConnectChan: chan N masked while in use" and
1854 * "ioi_masked$timer: Timeout on channel" console messages.
1855 * After the terminate interrupt, Multics re-issues the READ command and
1856 * want_to_read_since is reset, so the cycle repeats quietly every
1857 * NCP_ABSENT_TIMEOUT_SECS seconds until the NCP connects. */
1858 if (mgp_dev_state.pipe == NULL && !mgp_dev_state.eof_draining)
1859 {
1860 /* ncp_connect() handles PTW backoff and rate-limiting internally. */
1861 ncp_connect();
1862 if (mgp_dev_state.pipe == NULL)
1863 {
1864 /* Still not connected. Check whether we have been waiting too long. */
1865 time_t now2 = time(NULL);
1866 if (now2 - mgp_dev_state.want_to_read_since >= NCP_ABSENT_TIMEOUT_SECS)
1867 {
1868 sim_debug(DBG_DEBUG, &mgp_dev,
1869 "%s:%d: NCP absent for %d+ seconds, releasing IOM channel %d "
1870 "via terminate interrupt\r\n",
1871 __func__, __LINE__, NCP_ABSENT_TIMEOUT_SECS,
1872 mgp_dev_state.read_unit_chan);
1873 mgp_dev_state.want_to_read = 0;
1874 send_terminate_interrupt(mgp_dev_state.read_unit_idx,
1875 mgp_dev_state.read_unit_chan);
1876 }
1877 return;
1878 }
1879 /* NCP just connected. Reset the timestamp so we don't immediately
1880 * time out on the next invocation. */
1881 mgp_dev_state.want_to_read_since = time(NULL);
1882 }
1883
1884 /* REVERTED (2026-07-24) -- the advance_retry_pending mechanism that used to
1885 * live here (added 2026-07-23 alongside this same commit's read_full()/
1886 * resync-on-error improvements below, which are kept) turned out to cause
1887 * a worse bug than the one it fixed. On failure it retried by calling
1888 * iom_continue_channel() again on the next tick -- but iom_continue_channel()
1889 * unconditionally calls iom_list_service() as its first step, which fetches
1890 * the next DCW and advances LPW_DCW_PTR regardless of what happens
1891 * afterward. Each retry therefore advanced the DCW list an extra step with
1892 * no corresponding fresh packet write, silently racing the channel's
1893 * reported progress (stat.offset / stop_buffer_number on the Multics side)
1894 * ahead of how many buffer slots actually held fresh data. Confirmed via
1895 * mgp_daemon.log tracing on system-a/system-b: this caused Multics's
1896 * mgp_read_dcm_ catch-up loop to sweep past slots last written up to
1897 * buffer_size (16) delivery cycles ago and silently reprocess the stale
1898 * packet still sitting there, producing a steady-state "NAK from Multics"
1899 * storm recurring every ~3 seconds indefinitely, including at idle, and a
1900 * failed CFTP binary transfer. Reverting this mechanism (this branch) fixed
1901 * both directions of CFTP with no NAK storm, and reverified that fast-typing
1902 * character loss over chtn (the bug this was meant to fix) does NOT
1903 * resurface -- that fix already landed independently the day before
1904 * (2026-07-22) in mgp_queue_manager_.pl1's find_packet_connection and
1905 * mgp_read_dcm_.pl1's do-while loop, so this mechanism was redundant as well
1906 * as buggy. A defense-in-depth fix was also added on the Multics side
1907 * (mgp_read_dcm_.pl1 zeroes buffer_data(1) after a successful read) to
1908 * harmlessly skip any future stale-slot revisit regardless of cause, and is
1909 * kept independent of this revert.
1910 */
1911
1912 /* Keep one buffer free: if Multics hasn't read the buffer after this one,
1913 * leave the packet queued until it has (see mgp_read_ring_state). Only
1914 * meaningful while the channel is in use; the in_use guard below handles
1915 * the other case. Holding counts as no progress, so if Multics never
1916 * reads on, the watchdog above recovers after 60 s; it closes the NCP
1917 * socket and the reconnect empties in_buffer, so the held packets are
1918 * then lost, as on the other paths that close the socket. */
1919 enum mgp_ring_state ring = RING_NOT_FULL;
1920 word36 bad_word = 0;
1921 if (p->in_use)
1922 ring = mgp_read_ring_state(iom_unit_idx, chan, &bad_word);
1923 if (ring == RING_UNKNOWN)
1924 {
1925 if (bad_word)
1926 (void)sir_warn("%s:%d: can't check the read ring on chan %d (next DCW, at or "
1927 "via %#o, is not an IDCW: %012llo) - closing NCP socket and "
1928 "sending terminate interrupt to allow Multics channel recovery",
1929 __func__, __LINE__, chan, p->LPW_DCW_PTR,
1930 (unsigned long long) bad_word);
1931 else
1932 (void)sir_warn("%s:%d: can't check the read ring on chan %d (next DCW at or "
1933 "via %#o can't be read or followed, mode %d) - closing NCP "
1934 "socket and sending terminate interrupt to allow Multics "
1935 "channel recovery",
1936 __func__, __LINE__, chan, p->LPW_DCW_PTR, (int) p->chanMode);
1937 mgp_dev_state.ring_full = 0;
1938 mgp_reset_read_channel();
1939 return;
1940 }
1941 if (ring == RING_FULL)
1942 {
1943 if (!mgp_dev_state.ring_full)
1944 {
1945 sim_debug(DBG_DEBUG, &mgp_dev,
1946 "%s:%d: read ring full on chan %d; holding packets until Multics reads\r\n",
1947 __func__, __LINE__, chan);
1948 }
1949 mgp_dev_state.ring_full = 1;
1950 return;
1951 }
1952 mgp_dev_state.ring_full = 0;
1953
1954 /* Try to receive a packet from the NCP */
1955 u_char pkt8[MAX_PKT_BYTES];
1956 int pktlen = ncp_recv_packet(pkt8, MAX_PKT_BYTES);
1957 ncp_resume_reading_if_possible();
1958
1959 if (pktlen <= 0)
1960 {
1961 return; /* nothing available or error */
1962 }
1963
1964 /* pkt8 is the 8-bit wire format documented in multics_ncp/src/mgp.rs:
1965 * byte 2 = packet_type, byte 4 = frame_number, byte 16 = first data byte. */
1966 sim_debug (DBG_DEBUG, &mgp_dev,
1967 "%s:%d: recv pktlen=%d type=%d frame=%d data0=0x%02x('%c')\r\n",
1968 __func__, __LINE__, pktlen,
1969 pktlen > 2 ? pkt8[2] : -1,
1970 pktlen > 4 ? pkt8[4] : -1,
1971 pktlen > 16 ? pkt8[16] : 0,
1972 (pktlen > 16 && pkt8[16] >= 32 && pkt8[16] < 127) ? pkt8[16] : '.');
1973
1974 sim_debug(DBG_DEBUG, &mgp_dev,
1975 "%s:%d: received %d bytes from NCP for unit %d chan %d\r\n",
1976 __func__, __LINE__, pktlen, iom_unit_idx, chan);
1977
1978 /* Guard: only deliver if the IOM channel has an active, pinned I/O
1979 * operation. ioi_$workspace wires all workspace pages (DCW list, IDCWs,
1980 * DDCWs, and data buffers) while in_use=true. When in_use=false the
1981 * channel has no active I/O: DDCW_ADDR and DDCW_TALLY may be stale or
1982 * corrupted (e.g., left over from the previous iom_continue_channel call),
1983 * and the workspace pin is not guaranteed. Attempting delivery in this
1984 * state risks writing to a bad address.
1985 *
1986 * want_to_read=1 with in_use=false is the pathological state that produces
1987 * the overnight "fetch_IDSPTW: addr 07766 ptw 000000000000" cascade: mgp_cmd
1988 * was called via iom_continue_channel and stored a stale DDCW_ADDR, then
1989 * send_terminate_interrupt set in_use=false without clearing want_to_read.
1990 * The fix: if in_use is false, discard the packet, clear want_to_read, and
1991 * wait for Multics to re-issue ioi_$connect (which will set in_use=true and
1992 * establish a fresh, valid DDCW for us). */
1993
1994 if (! p->in_use)
1995 {
1996 (void)sir_warn("%s:%d: chan %d not in active I/O (in_use=false) but want_to_read=1"
1997 " - discarding packet, clearing want_to_read",
1998 __func__, __LINE__, chan);
1999 mgp_validate_dcw_state(iom_unit_idx, chan, 129, "process_event-in_use=0");
2000 mgp_dev_state.want_to_read = 0;
2001 return;
2002 }
2003
2004 /* Convert 8-bit packet to 36-bit words */
2005 word36 buffer[128]; /* 4 header + up to 122 data words */
2006 uint words_processed = 128;
2007 (void)memset(buffer, 0, sizeof(buffer));
2008
2009 pkt8_to_word36(pkt8, pktlen, buffer, 128);
2010
2011 sim_debug(DBG_DEBUG, &mgp_dev,
2012 "%s:%d: received %d bytes from NCP for unit %d chan %d\r\n",
2013 __func__, __LINE__, pktlen, iom_unit_idx, chan);
2014
2015 # if defined(TESTING)
2016 if (sim_deb && (mgp_dev.dctrl & DBG_DEBUG))
2017 {
2018 dumppkt("NCP-Read", buffer, words_processed);
2019 }
2020 # endif
2021
2022 /* Validate the IOM DMA target PTW before writing packet data.
2023 *
2024 * This is a belt-and-suspenders check that runs AFTER the in_use guard
2025 * above. If in_use=true, the workspace IS pinned and DDCW_ADDR should
2026 * be valid - but if the DCW list became corrupted (e.g., circular-buffer
2027 * wrap producing a bad DDCW), DDCW_ADDR might point outside the workspace.
2028 * The workspace has only 1 IOM page (820 words for buffer_size=6,
2029 * data_size=129), so any DDCW_ADDR >= 01400 octal (page 1+) has no PTW
2030 * entry and PTW=0. The stale value "07766" seen in overnight cascades is
2031 * page 3 - far beyond the workspace - and is caught here.
2032 *
2033 * Calling iom_indirect_data_service() with PTW=0 would compute physical
2034 * address ~= 0 and overwrite the IOM mailbox area (addresses 0-0777),
2035 * causing an IOM interrupt storm -> Multics CPU at 100%.
2036 *
2037 * If invalid: close the NCP socket (discarding all queued NOOPs from the
2038 * kernel receive buffer), send one clean terminate interrupt so Multics
2039 * can re-establish the channel, and return without doing the DMA.
2040 *
2041 * max_words=256: cap the PTW range check at 256 words (generous upper
2042 * bound for any NCP packet: header 16 bytes + max data 488 bytes = 504
2043 * bytes = 126 36-bit words, rounded up). This prevents false positives
2044 * when DDCW_TALLY=0 (IOM interprets as 4096 words, spanning pages 0-3)
2045 * which can occur if iom_continue_channel left the channel in a
2046 * transitional "uff or nothing to send" state. With max_words=256 and
2047 * DDCW_ADDR=0: end_page=(0+255)/1024=0 -> only page 0 checked -> valid.
2048 * Real corruption (DDCW_ADDR=07766 = page 3) is still caught because
2049 * start_page=3 and that page has no PTW entry. */
2050 if (!mgp_check_dma_ptw(iom_unit_idx, chan, 256))
2051 {
2052 (void)sir_warn("%s:%d: invalid DMA PTW on chan %d - closing NCP socket and "
2053 "sending terminate interrupt to allow Multics channel recovery",
2054 __func__, __LINE__, chan);
2055 mgp_reset_read_channel();
2056 return;
2057 }
2058
2059 /* Validate DDCW_ADDR is within the read channel's data buffer area.
2060 *
2061 * ROOT CAUSE FIX for the DDCW_ADDR=0 / DCW-list corruption bug:
2062 *
2063 * After all 6 buffers are delivered, iom_continue_channel reaches the TDCW
2064 * at DCW list offset 12. iom_list_service calls unpack_DCW for the TDCW
2065 * word; since the TDCW has DATA_ADDRESS=0 (all zero bits, as initialized
2066 * by mgp_read_dcm_.pl1 initialize_workspace), unpack_DCW stores
2067 * p->DDCW_ADDR = 0. LPW_TALLY then decrements to 0, setting uff=true.
2068 * iom_continue_channel sees uff=true, logs "uff or nothing to send", and
2069 * returns WITHOUT calling mgp_cmd(001) and WITHOUT sending a terminate
2070 * interrupt. want_to_read=1 persists with p->DDCW_ADDR=0.
2071 *
2072 * The PTW check above passes for DDCW_ADDR=0 with max_words=256 because
2073 * workspace page 0 IS a valid mapped page - the workspace is only 1 IOM
2074 * page. Without this range check, iom_indirect_data_service(write=true)
2075 * would write 128 words of NCP packet data to workspace offset 0,
2076 * overwriting the entire DCW list (IDCW/DDCW pairs at offsets 0-11).
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 MGP_FIRST_BUFFER_OFFSET (22), which is the
2083 * start of buffer(0) in the workspace. Send a terminate interrupt so
2084 * Multics re-issues ioi_$connect; the fresh mgp_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 < MGP_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; NCP socket stays connected",
2093 __func__, __LINE__, p->DDCW_ADDR, chan,
2094 MGP_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 NCP socket. The TDCW wrap is a normal, periodic
2099 * IOM event (happens after every 6th buffer delivery). The socket
2100 * is healthy - only the IOM channel state needs resetting. Closing
2101 * the socket would cause unnecessary NCP reconnect cycles (~every 6s)
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 mgp_cmd(001)/get_ddcw() sets a valid
2105 * DDCW_ADDR (>= 22), and delivery resumes on the next NCP packet. */
2106 mgp_dev_state.want_to_read = 0;
2107 send_terminate_interrupt(mgp_dev_state.read_unit_idx,
2108 mgp_dev_state.read_unit_chan);
2109 return;
2110 }
2111
2112 /* Write the packet to the current IOM workspace buffer[N]. */
2113 iom_indirect_data_service(
2114 iom_unit_idx, chan, buffer, &words_processed, true);
2115
2116 /* Real forward progress: feed the no-progress watchdog above. */
2117 mgp_dev_state.last_progress_time = time(NULL);
2118
2119 sim_debug(DBG_DEBUG, &mgp_dev,
2120 "%s:%d: wrote %d words to IOM buffer, advancing channel and sending marker interrupt\r\n",
2121 __func__, __LINE__, words_processed);
2122
2123 /*
2124 * Advance the channel to workspace buffer[N+1] and send a marker
2125 * interrupt. iom_continue_channel() does the following in order:
2126 *
2127 * 1. Calls iom_list_service() to fetch IDCW[N+1] from the DCW list
2128 * (LPW_DCW_PTR advances from IDCW[N+1] to DDCW[N+1]).
2129 * 2. Calls d->iom_cmd() for IDCW[N+1]:
2130 * mgp_cmd(read) -> get_ddcw() -> iom_list_service() reads DDCW[N+1]
2131 * -> DDCW_ADDR = buffer[N+1], LPW_DCW_PTR = IDCW[N+2].
2132 * mgp_cmd sets want_to_read=1 and returns IOM_CMD_PENDING.
2133 * 3. Calls send_marker_interrupt():
2134 * stat.offset = LPW_offset(IDCW[N+2]) - 1 = 2*(N+2) - 1
2135 * stop_buffer = divide(stat.offset, 2) = N+1
2136 * mgp_read_dcm_'s loop fires (buffer_number=N != stop_buffer=N+1),
2137 * calls mgp_read_dim_(N), then increments buffer_number to N+1.
2138 *
2139 * start_io sees running=true (marker) and does NOT reconnect; the
2140 * channel stays pending with DDCW_ADDR pointing to buffer[N+1].
2141 * want_to_read remains 1 (set inside mgp_cmd via iom_continue_channel).
2142 *
2143 * On failure (e.g. DCW list corrupt, "expected IDCW"): iom_continue_channel
2144 * just returns without calling mgp_cmd and without sending any interrupt.
2145 * We detect this via delivery_succeeded: mgp_cmd(READ) sets it to 1 on
2146 * success; we clear it just before calling iom_continue_channel. After
2147 * 3 consecutive failures we reset want_to_read, close the NCP socket,
2148 * and send a terminate interrupt so Multics can re-establish the channel.
2149 */
2150 mgp_dev_state.delivery_succeeded = 0;
2151 int rc = iom_continue_channel(iom_unit_idx, chan);
2152
2153 sim_debug (DBG_DEBUG, &mgp_dev,
2154 "%s:%d iom_continue_channel rc=%d delivery_succeeded=%d\r\n",
2155 __func__, __LINE__, rc, mgp_dev_state.delivery_succeeded);
2156
2157 /* Detect and break "expected IDCW" cascade. */
2158 {
2159 static int consecutive_iom_failures = 0;
2160 if (mgp_dev_state.delivery_succeeded)
2161 {
2162 consecutive_iom_failures = 0;
2163 }
2164 else
2165 {
2166 if (++consecutive_iom_failures >= 3)
2167 {
2168 (void)sir_warn("%s:%d: %d consecutive IOM delivery failures on chan %d; "
2169 "resetting want_to_read, closing NCP socket, and "
2170 "sending terminate interrupt",
2171 __func__, __LINE__, consecutive_iom_failures,
2172 mgp_dev_state.read_unit_chan);
2173 consecutive_iom_failures = 0;
2174 mgp_dev_state.want_to_read = 0;
2175 /* Close socket to discard remaining queued NCP packets;
2176 * see the comment in the PTW-check block above. */
2177 if (mgp_dev_state.pipe != NULL)
2178 {
2179 if (mgp_dev_state.pipe) //-V547
2180 {
2181 uv_close((uv_handle_t *)mgp_dev_state.pipe, ncp_close_cb);
2182 mgp_dev_state.pipe = NULL;
2183 }
2184 }
2185 if (rc == 0) /* we handle the rc != 0 case below */
2186 send_terminate_interrupt(mgp_dev_state.read_unit_idx,
2187 mgp_dev_state.read_unit_chan);
2188 }
2189 }
2190 }
2191
2192 /* if iom_continue_channel returned a fatal error, terminate the I/O and let the DCM
2193 restart it. */
2194 if (rc != 0)
2195 {
2196 send_terminate_interrupt(mgp_dev_state.read_unit_idx,
2197 mgp_dev_state.read_unit_chan);
2198 }
2199 }
2200
2201 #endif /* if defined(WITH_MGP_DEV) */