Symptom
A guest OS (TurboDOS 1.43 slave) connects as a NET client to a server
machine whose guest only services the first server port pair
(status/data 0x28/0x29). Roughly 1 boot in 15, the slave hangs
forever waiting for its first reply while both machines look
perfectly healthy. The TCP connection is established; the client's
bytes simply arrive in the buffers of the SECOND server port pair
(0x2A/0x2B), which the server guest never reads. Nothing is logged
(unless NET debug is enabled -- which changes the poll cadence enough
that the fault also stops reproducing).
Cause
net_svc()'s server-side accept scan calls sim_accept_conn() once per
FREE slot, not once per poll:
for (i = 1; i <= MAX_CONNECTIONS; i++)
if (serviceDescriptor[i].ioSocket == 0) {
s = sim_accept_conn(serviceDescriptor[1].masterSocket, NULL);
if (s != INVALID_SOCKET) {
serviceDescriptor[i].ioSocket = s;
...
With both slots free the loop makes TWO accept calls per poll. A TCP
connection that becomes acceptable between the i=1 call and the i=2
call is accepted by the second call and lands in slot 2 -- even
though slot 1 is free. The Z80-visible port pair a client is bound to
is therefore a race against the poll loop, not the deterministic
"first connection -> first port pair" a guest configuration must
assume (the slot decides which status/data ports carry the traffic,
and guests are configured statically around exactly that).
Once slot 1 is occupied the behaviour is deterministic, which is why
multi-client setups mostly work and the fault presents as a rare
single-client/first-client startup failure.
Demonstration
With ONE client connecting to a freshly attached server, debug flags
enabled, the accept log reads:
NET ACCEPT: NET: [0x0000ec7b] Accepted connection 2 with socket 10.
(one client, empty slot 1, assigned to connection 2.)
Suggested fix
Accept at most one connection per poll and assign it to the first
free slot -- e.g.:
s = sim_accept_conn(serviceDescriptor[1].masterSocket, NULL);
if (s != INVALID_SOCKET) {
for (i = 1; i <= MAX_CONNECTIONS; i++)
if (serviceDescriptor[i].ioSocket == 0) {
serviceDescriptor[i].ioSocket = s;
sim_debug(...);
break;
}
/* no free slot: close s */
}
This preserves current behaviour in every case except the race, makes
slot assignment deterministic (connection order = slot order), and
removes the double accept() per poll.
Context
Found while running a genuine TurboDOS 1.43 master with two slaves
over the NET device (master = server: circuit A on 0x28/0x29, circuit
B on 0x2A/0x2B; slaves = clients). The guest network stalls this race
produces were indistinguishable from guest OS bugs until the wire-
level accept log was read. Report prepared with the assistance of
Claude (Anthropic) during that investigation; the analysis was
verified against open-simh master by human-directed testing.
Symptom
A guest OS (TurboDOS 1.43 slave) connects as a NET client to a server
machine whose guest only services the first server port pair
(status/data 0x28/0x29). Roughly 1 boot in 15, the slave hangs
forever waiting for its first reply while both machines look
perfectly healthy. The TCP connection is established; the client's
bytes simply arrive in the buffers of the SECOND server port pair
(0x2A/0x2B), which the server guest never reads. Nothing is logged
(unless NET debug is enabled -- which changes the poll cadence enough
that the fault also stops reproducing).
Cause
net_svc()'s server-side accept scan calls sim_accept_conn() once per
FREE slot, not once per poll:
With both slots free the loop makes TWO accept calls per poll. A TCP
connection that becomes acceptable between the i=1 call and the i=2
call is accepted by the second call and lands in slot 2 -- even
though slot 1 is free. The Z80-visible port pair a client is bound to
is therefore a race against the poll loop, not the deterministic
"first connection -> first port pair" a guest configuration must
assume (the slot decides which status/data ports carry the traffic,
and guests are configured statically around exactly that).
Once slot 1 is occupied the behaviour is deterministic, which is why
multi-client setups mostly work and the fault presents as a rare
single-client/first-client startup failure.
Demonstration
With ONE client connecting to a freshly attached server, debug flags
enabled, the accept log reads:
(one client, empty slot 1, assigned to connection 2.)
Suggested fix
Accept at most one connection per poll and assign it to the first
free slot -- e.g.:
This preserves current behaviour in every case except the race, makes
slot assignment deterministic (connection order = slot order), and
removes the double accept() per poll.
Context
Found while running a genuine TurboDOS 1.43 master with two slaves
over the NET device (master = server: circuit A on 0x28/0x29, circuit
B on 0x2A/0x2B; slaves = clients). The guest network stalls this race
produces were indistinguishable from guest OS bugs until the wire-
level accept log was read. Report prepared with the assistance of
Claude (Anthropic) during that investigation; the analysis was
verified against open-simh master by human-directed testing.