Skip to content

test_typed_target: restore A/X/Y around each hijacking call - #283

Merged
JC-000 merged 1 commit into
masterfrom
fix/typed-target-flake
Oct 6, 2026
Merged

JC-000 merged 1 commit into
masterfrom
fix/typed-target-flake

Conversation

@JC-000

@JC-000 JC-000 commented Oct 6, 2026

Copy link
Copy Markdown
Owner

Mechanism

test_typed_target.py on uci-comb could fail a different, random number of cases on the same PRG: 139/139 on one run, 92/139 and 108/139 on others. Every refused case after a certain point printed COLD BANK FAIL with cold_err = 9 (COLD_R_BUSY).

  • The harness's jsr() restores PC, SP and FL after a call. It does not restore A, X or Y.
  • Each refused case lets main_loop run for 3 s. The next case then halts the CPU at a random instruction and calls into the program from there.
  • If the halt lands on net_poll's cmp #NET_TCP_CONNECTED (src/net/uci/net.s, right after lda net_tcp_state), A comes back holding the suite driver's carry.
  • After a refused do_https_get that carry is 1, which is NET_TCP_CONNECTED.
  • On resume, net_poll tries to read a socket that VICE does not have, and net_tcp_state stays at ERROR from then on.
  • cold_call's busy check (src/net/uci/cold_bank.s) then turns down every later prompt.

The busy check is behaving correctly; the test was handing it a wrong socket state. On a real machine nothing changes A between those two instructions.

Fix: call() saves the registers before jsr() and puts A, X and Y back afterwards.

Evidence (master 249f65d, uci-comb, VICE)

  • Forced-park red/green. A checkpoint parks the CPU on net_poll+3 before a refused case:
    • Without this change: net_tcp_state goes 0 → 2, and the next case gets cold_err=9 and fails 3 of 6 checks (9/12).
    • With this change: net_tcp_state stays 0, cold_err=0, 12/12.
    • An independent adversarial reviewer reproduced both results.
  • Full suite. With this change, 139/139 in 3 runs out of 3.

Only the test changes. No product source changes, so no PRG changes.

🤖 Generated with Claude Code

jsr() puts back PC/SP/FL after the call but leaves A/X/Y as the routine
returned them. Every case after the first refusal resumes main_loop and
then halts it at a random instruction. Halted on net_poll's
`cmp #NET_TCP_CONNECTED` after a refused do_https_get, A is the driver's
carry, 1 = NET_TCP_CONNECTED. The resume then polls a socket VICE does
not have, net_poll latches net_tcp_state = ERROR, and cold_call's busy
guard refuses every later prompt (COLD BANK FAIL, cold_err = 9). That is
the 92/139 and 108/139 cascade on uci-comb, and the 139/139 runs where
the halt missed that instruction.

Test defect, not product: nothing on a real machine changes A between
those two instructions.

Red-green evidence: parking the CPU at net_poll+3 before a refused case
gives net_tcp_state=2 and cold_err=9 on the next case (9/12) without
this change, and 12/12 with net_tcp_state=0 with it. Full suite on
uci-comb: 139/139, three runs out of three.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
JC-000 added a commit that referenced this pull request Oct 6, 2026
…dv-d2 F3)

In VICE the trust store read fails (no UCI), so trust_pre asks CONTINUE
UNPINNED? Y/N before the dial, after flushing the key buffer; the suite
never answered and hung on every shipped TRUST_STORE image. On those
images do_https_get now runs until it returns or parks on the question,
types Y (so every accepted target still reaches the dial stub), and
checks that it was asked; a refused target must never be asked. One more
case answers N: NOT DIALLED, nothing dialled. call() is untouched (#283).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@JC-000
JC-000 merged commit 96a3f18 into master Oct 6, 2026
JC-000 added a commit that referenced this pull request Oct 6, 2026
…dv-d2 F3)

In VICE the trust store read fails (no UCI), so trust_pre asks CONTINUE
UNPINNED? Y/N before the dial, after flushing the key buffer; the suite
never answered and hung on every shipped TRUST_STORE image. On those
images do_https_get now runs until it returns or parks on the question,
types Y (so every accepted target still reaches the dial stub), and
checks that it was asked; a refused target must never be asked. One more
case answers N: NOT DIALLED, nothing dialled. call() is untouched (#283).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@JC-000
JC-000 deleted the fix/typed-target-flake branch October 14, 2026 15:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant