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