Skip to content

net_tcp_connect (4 exits) and net_tcp_close (1) leave the UCI interface mid-transaction on a timeout, bricking it until reboot #221

Description

@JC-000

net_tcp_connect (4 exits) and net_tcp_close (1) leave the UCI interface mid-transaction on a timeout, bricking it until reboot

Summary

Sibling of #194, deliberately scoped out of its fix. #194 closes three net_tcp_send timeout exits that returned without clearing the command interface. The same defect exists, unfixed, at five more sites:

  • net_tcp_connect — four structurally identical dirty exits: the entry uci_wait_idle bail, the uci_push_wait bail, @tc_err_drain_to, and @tc_ok_drain_to.
  • net_tcp_close@cl_drain_to.

All in src/net/uci/net.s.

Why these were left out of #194, and why that was right

#194 was scoped to the $86 mechanism, and for that purpose the scope is exactly correct — verified during its adversarial review, against the firmware source rather than inference:

  • Only net_poll reaches PUSH_CMD via uci_wait_not_busy (mask $01). net_tcp_send, net_tcp_connect and net_tcp_close all open with uci_wait_idle (mask $31), so a left-open data phase blocks them rather than producing a rejected push.
  • Only a net_tcp_send bail leaves net_tcp_state at NET_TCP_CONNECTED, so polling continues afterwards and meets the dirty interface.

So connect/close cannot produce $86. That is why they are not part of #194 and should not be retro-fitted into it.

Why they still matter

They can brick the interface until reboot. A @tc_ok_drain_to bail leaves the interface in state "10" with the data phase open. Every subsequent net_tcp_connect then burns its full 5 s in uci_wait_idle and returns CONNECT_FAIL — permanently, for the rest of the session. Before #194, uci_abort had exactly one call site in the entire codebase (net_init), so the only thing that ever cleared a mid-transaction bail was the next boot.

The user-visible shape is "the machine connects fine, then stops being able to connect at all, and a reset fixes it" — which is easy to misfile as a device wedge. This project has a documented history of misdiagnosing interface-level faults as firmware corruption, so a mechanism that produces exactly that signature is worth closing.

Suggested fix

@sb_bail from #194 is the ready-made remedy: write ABORT, then a bounded uci_wait_idle, carry discarded (the expiring wait has already set net_last_error = $89). Route all five exits to an equivalent shared bail.

Notes carried over from #194's review, so they are not re-derived:

  • ABORT, not DATA_ACC. DATA_ACC is gated on state(1) = '1' in command_protocol.vhd:163, so it is a no-op on a push-wait exit (state "01"). The abort bit sets handshake_in(2) unconditionally (:168-170); the FreeRTOS task calls target->abort() and writes HANDSHAKE_RESET (0x87, command_intf.h:45), whose bit 7 hits the state <= "00" arm (:227-232).
  • ABORT does not close sockets. NetworkTarget::abort() (network_target.cc:494) only calls discard_read_reply(); close_all_sockets() is reached solely from c64_reset(). No socket desync, fw #814's socket table unaffected.
  • HANDSHAKE_RESET also resets command_pointer, which DATA_ACC does not.
  • uci_abort's own 32-iteration settle (~160 cycles) is far shorter than one uci_fence (~5,450 cycles at OUTER=5 × INNER=217), so it cannot cover a FreeRTOS round trip on its own — the bounded uci_wait_idle after it is load-bearing.

Known limitation the fix will inherit

From #194's review: the abort is serviced by the same FreeRTOS "UCI Server" task that services commands (command_intf.cc:136-143). If that task is blocked in lwip_send — the likeliest reason the wait expired in the first place — the ABORT cannot be actioned either, the follow-up uci_wait_idle also expires, and the interface is still dirty with ~5 s more spent. This class of fix helps transient slowness only. Worth stating in the fix rather than discovering later.

Also inherited: each patched exit adds up to one extra bounded 5 s wait to a failure path. In net_tcp_send that was measured as one extra wait per call (the bail exits the routine, not the chunk loop), with no caller retrying. Confirm the same holds for connect/close before assuming it.

Cost estimate

#194 cost +1 byte of UCI_CODE for three exits sharing one bail, with every other segment byte-identical and all four ip65 PRGs unchanged. Five more exits sharing the same bail should be comparably cheap, but measure it: ip65 is unaffected (nothing outside src/net/uci/ moves), while UCI NET_CODE tail free was 365 B after #194.

Testing

tools/test_uci_timeout_recovery.py (added by #194) runs the shipped 6502 bytes against a model of $DF1B-$DF1F built from command_protocol.vhd and command_intf.cc, with a delayed-response firmware and a running CIA1 TOD, so a recovery that aborts without waiting does not pass by accident. Extend it rather than writing a second harness. Note its documented limits: the model's "5 s" budget is really ~50 loop iterations, and it proves the code waits at all rather than that a real abort round-trip fits in the bound.

These paths are not reachable on demand on hardware, so a deterministic test is the primary evidence and any hardware run is happy-path confirmation only.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions