Skip to content

AltairZ80 NET device: accept can assign an incoming connection to the wrong slot (server port pair) #573

Description

@gulsko

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions