Skip to content

UCI: three net_tcp_send timeout exits return without DATA_ACC, leaving the FPGA out of idle (plausible $86 source) #194

Description

@JC-000

Claim

PLAUSIBLE ONLY — needs hardware to confirm or refute. Filing so it is not
lost, not asserting it is a live bug.

net_tcp_send has three exit paths that return C=1 without ever reaching
uci_ack, i.e. without the DATA_ACC that returns the FPGA's state machine to
idle. Per the mechanism in #144, a transaction left un-accepted makes the next
PUSH_CMD hit else error_busy <= '1' and be silently dropped — which is
$86 on the $DF1C status bit, a code CLAUDE.md records as having been seen
in the field on otherwise-passing runs.

Verified against origin/master at dc06095, src/net/uci/net.s:

exit lines reached when
@sb_push's bail :817-818 uci_push_wait timed out waiting for the SOCKET_WRITE response
@sb_err_drain_to :832-834 uci_drain_resp/uci_drain_status timed out on the UCI_ERR_SEND_FAIL path
@sb_ok_drain_to :852-854 the same two drains timed out on the success path

All three are wall-clock-timeout paths with net_last_error already set to
UCI_ERR_WAIT_TIMEOUT ($89), so they are honest failures — the question is
only what state they leave the device in for the next command.

The recovery primitive exists and is not wired to any of them:

$ grep -n 'jsr uci_abort' src/net/uci/net.s
107:        jsr uci_abort

One call site, in net_init. Nothing on an error path calls it, so the only
thing that clears the state machine after a mid-transaction bail is the next
boot.

Why this is not a #144 reopen

#144's three named defects are all closed on master and I verified each
independently before closing it (see
#144 (comment)): the
per-byte pulse is gone, all 11 uci_ack sites run
drain_resp -> drain_status -> ack in that order, and the SOCKET_WRITE path
has its own accept on both its completing exits. This is a fourth thing,
about timeout paths rather than about queue semantics, and it is defensible as
designed — a wedged FPGA may not answer an accept either, and issuing one into
an unknown state is not obviously safer than not.

What would settle it

Hardware, and it is cheap to look for because the symptom is already
instrumented:

  • Supports the claim: after any run that records $89 UCI_ERR_WAIT_TIMEOUT
    on a send, the next transaction reports $86. CLAUDE.md notes $86 has
    exactly one setter in command_protocol.vhd (the PUSH_CMD-while-not-idle
    branch), so it always means our push was rejected — the two appearing in that
    order would be the signature.
  • Refutes it: $89 on a send with no $86 following, or $86 observed on
    runs with no preceding send timeout.

CLAUDE.md's note that $86 has been seen on passing runs (github.com HTTP 200,
2026-08-28) is the reason this is worth checking rather than dismissing: a
passing run that carries one recovered from a real rejected push, and this is
a candidate source for it.

If confirmed, the fix is presumably jsr uci_abort before the sec; rts on
the two drain-timeout exits (@sb_err_drain_to, @sb_ok_drain_to) and
possibly on @sb_push's. uci_abort already writes ABORT and burns a settle
delay, so it is the intended primitive; it just has one caller.

No hardware was available to this session.

Found by adversarial review while confirming #144's closure.

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