Skip to content

uci: unfence UCI register reads in the 6510 products (wiki 375->175 s onchip, 364->163 s comb on U64E); UCI_READ_FENCE=1 restores them - #299

Merged
JC-000 merged 4 commits into
masterfrom
perf/uci-read-fence
Oct 14, 2026
Merged

JC-000 merged 4 commits into
masterfrom
perf/uci-read-fence

Conversation

@JC-000

@JC-000 JC-000 commented Oct 14, 2026 •

Copy link
Copy Markdown
Owner

What

BACKEND=uci (the shipped 6510 TLS products) put a uci_fence after every UCI register access, reads included. This PR removes the fence after reads of $DF1C-$DF1F in src/net/uci: 15 sites, 5 in net.s and 10 in uci_cmd.s. They now use uci_read_fence, or an .ifdef around their jsr uci_settle. It is the same change #296 made for uci-m3.

Unchanged:

  • Every write fence, the push rule, the shared uci_fence macro and every TOD-bounded wait.
  • The one fence net_poll keeps before its header read. It is now labelled uci_poll_stage_fence; the label adds no bytes.
  • The trust store (uci_dos.s) is affected too, corrected after review (adv-299 finding 1).
    • uci_dos.s keeps the fence after its own three reads, now labelled uci_dos_rd_part, _data and _state (labels, no bytes).
    • But every DOS transaction also runs through the primitives this PR unfenced: uci_wait_idle, uci_wait_reply, uci_check_err, uci_drain_resp and uci_drain_status. So the store's traffic, which trust_pre runs on every 'G' in both shipped products, has unfenced reads on the default image, and new read→write adjacencies.
    • It is tested as such: model below, and hardware (C64U section).

The stage fence and the DOS path are consistent in effect. After uci_check_err's status read:

  • net_poll runs uci_poll_stage_fence, then reads $DF1C and the data.
  • dos_part reads $DF1C and then fences (its own jsr uci_settle) before the first data read.

On both paths exactly one fence sits between the error check and the first response-data read. Whether either fence is needed was not tested; both are kept as they were on master. The stage fence is the only fence after a read left on the network path, and the checker exempts it by label.

Knob and hashes (rebased on master a7bccc9; clean builds)

UCI_READ_FENCE=1 (BACKEND=uci only, refused elsewhere, recorded in flags.stamp) assembles master byte for byte:

build master a7bccc9 this PR UCI_READ_FENCE=1 this PR default
uci-onchip product (USE_NISTCURVES_ONCHIP=1 TRUST_STORE=1) fd37600b fd37600b ffa47fc3
uci-comb product (USE_NISTCURVES_ONCHIP_COMB=1 TRUST_STORE=1) 025af934 025af934 67cb0048
plain BACKEND=uci 806fda4c 806fda4c ac427273
onchip, no store 6a1f52bd 6a1f52bd 924132de
comb, no store aa340d8f aa340d8f 697f18e1
onchip, no store, wiki REU sink 81d0abaf 81d0abaf 24415efb
comb, no store, wiki REU sink 94f0f3a4 94f0f3a4 67f9d2d4
ip65-onchip 810f1158 — 810f1158 (unchanged)
uci-m3 c64267c4 — c64267c4 (unchanged)

The REU-sink builds are HTTPS_BODY_TO_REU=1 HTTPS_HOST=en.wikipedia.org HTTPS_PATH=/wiki/Commodore_64.

Model + mutation (red/green, at this head)

tools/check_uci_read_pacing.py (new, standalone):

  • Runs the shipped adapter on test_uci_abort_recovery.py's interpreter and model through these flows: net_init; connect; data, idle, EOF and reset polls; an over-claimed header, which reaches uci_drain_resp's data read; send; close.
  • Logs every $DF1C-$DF1F access together with the uci_fence executions after it.
  • Every write must be fenced.
  • On the default image no read is fenced, except at uci_poll_stage_fence. On a UCI_READ_FENCE=1 image every read is fenced.
  • On a TRUST_STORE=1 image it also runs the store's DOS traffic on test_trust_store_6502.py's DOS model: a load on an empty medium, a stage + save, then a reload.
    • Those reads are held to the same rule.
    • uci_dos.s's own three reads must stay fenced on every image.
    • The flow fails if the DOS path never ran.
  • Results:
    • store images, default ffa47fc3 (onchip) and 67cb0048 (comb): 9/9; UCI_READ_FENCE=1 fd37600b: 9/9 (at 78e5aea);
    • plain ac427273 / 806fda4c: no store, so the trust flows are not applicable;
    • with the expectation flipped (--expect), it fails both ways.

tools/mutate_uci_read_pacing.sh (builds the onchip store image): 8/8 killed, control 9/9.

KILLED   put-unfenced:       init+connect: 3 write(s) with no fence after
KILLED   begin-unfenced:     init+connect: 1 write(s) with no fence after
KILLED   copy-read-fenced:   poll data: 64 fenced read(s) in the default image, first $DF1E
KILLED   status-wait-fenced: init+connect: 3 fenced read(s) in the default image, first $DF1C
KILLED   statline-fenced:    init+connect: 5 fenced read(s) in the default image, first $DF1F
KILLED   dos-own-unfenced:   trust load+save: 80 of uci_dos.s's own read(s) unfenced
KILLED   check-err-fenced:   init+connect: 1 fenced read(s) in the default image, first $DF1C
KILLED   drain-resp-fenced:  poll overclaim: 49 fenced read(s) in the default image, first $DF1E

The reverse direction at that site is red too: with uci_drain_resp's data read unfenced on a UCI_READ_FENCE=1 image, the overclaim flow reports 49 unfenced reads (8/9).

Suites on default BACKEND=uci:

  • pytest: 483 passed. The 1 failure is test_ip65_hw_checks_unit, which needs an ip65 build in build/; it fails the same way with master's bytes.
  • test_uci_abort_recovery 25/25, test_uci_resp_read_wait 6/6, test_uci_dhcp_probe 6/6.
  • test_trust_store_6502 128/128 on the onchip store image.
  • test_runner_coverage and test_pytest_boundary green.

What the detector cannot see is in its docstring: a delay that is not a uci_fence, and real FPGA timing. The hardware below covers timing.

Hardware

Protocol for every run:

  • DeviceLock, then prepare_device, then preflight_reu.
  • Same-session interleaved A/B (ABBAAB or FUUFFU).
  • Real-server runs: the clock runs from 'G' to the REU viewer (the whole body is in REU). The body is read back and compared with a host fetch made just before, with the same UA. The client's own framing verdict, net_last_error, tcp_recv_overflow are logged on every attempt.

C64 Ultimate: C64U fw 1.1.0 (stock), FPGA 122, core 1.49, at 10.43.23.26, 64 MHz. Built from this rebased tree (a7bccc9 base).

Prep set REU Enabled 16 MB and turbo 64 Manual before the reset. Wiki and robots.txt to REU, n=3 per cell, median (min-max) s:

page build fenced unfenced (this PR)
/robots.txt 28,275 B onchip (81d0abaf / 24415efb) 48.61 (47.84-48.95) 40.92 (40.90-41.72)
/robots.txt comb (94f0f3a4 / 67f9d2d4) 39.53 (39.41-40.01) 33.14 (32.31-34.39)
/wiki/Commodore_64 751,822 B onchip 280.25 (279.62-280.47) 129.77 (129.70-130.48)
/wiki/Commodore_64 comb 272.20 (271.28-272.35) 121.75 (121.60-123.19)
  • All 24 attempts: HTTP 200, net_last_error $00, tcp_recv_overflow 0, and complete by the client's framing.
  • robots.txt: every body is byte-equal to the host fetch (CRC 79C6EE37).
  • Wiki: every body has CRC 373614AA (751,822 B), metadata-only against its host reference. One comb-fenced attempt was served chunked and is complete by its terminal chunk, with the same bytes.
  • No unfenced failure on the C64U, so no read-fence bracketing was needed.
  • Harness note: on this stock firmware, the REST writemem that types 'Q' at the viewer often returned "connection dropped" (15 of 24). The key had landed every time: "CONNECTION CLOSED" was on screen within 60 s, and no socket was left open.

Local listener (rig_https_local.py, 64 MHz, HTTPS_SNI=www.foo.invalid no-store builds, DEBUG_CAPTURE=0, FUUFFU):

  • onchip (a665e6f9 fenced / 509a247c unfenced): 6/6 PASS, $00. Sentinel at 40.7-41.3 s fenced, 40.0-41.0 s unfenced.
  • comb (6e4c6c51 / ef103660): 6/6 PASS, $00. Sentinel at 32.8-33.3 s fenced, 32.2-32.9 s unfenced.
  • A first attempt at these local runs was refused by the harness before it touched the device: Ultimate64DeviceAliasError on run_prg. My wrapper also held the legacy raw-address lockfile for .26, and the harness read that as a second lock holder. The runs above were made without that wrapper.

C64U, store path and small-transaction stress (adv-299 findings 1 and 2; tree cdc35a3, i.e. this head's code; 64 MHz; one DeviceLock per attempt; F/U interleaved):

  • Storage: probed inside the DeviceLock through the harness storage API: volumes Flash, SD, Temp, writable SD. The C64U now has an SD card.
  • rig_trust_store.py, store at /SD/c64https-test, every check per run (24 on onchip, 25 on comb: the 25th, "cold bank: the image is in cert_buf", exists only there): load EMPTY, save, VALID, ping-pong, a torn write plus reset, FORMAT fallback, repair, CHECKSUM, VERSION, 32 records (a 2,064 B image: five 512 B read parts, nine write chunks), FULL, NOPATH.
    • onchip, fenced c9af5616 and unfenced 74d9c623: 24/24 PASS each.
    • comb (cold bank), unfenced b0bb6289 and fenced c11c09ff: 25/25 PASS each.
  • The SHIPPED images through the menu (rig_https_banner.py, typed en.wikipedia.org /robots.txt):
    • onchip fenced fd37600b ×2 and unfenced ffa47fc3 ×2; comb unfenced 67cb0048 ×1 and fenced 025af934 ×1.
    • The shipped store directory is /USB1, which the C64U does not have. So on every run, fenced and unfenced, trust_pre's DOS traffic runs and fails cleanly with the same outcome: TRUST FAIL 02 (R_NOPATH), the "CONTINUE UNPINNED? Y/N" prompt, then "NOT DIALLED" after the rig's 'Q'.
    • No connection was opened, and the result is identical across arms. The rig reports FAIL because no body was fetched; that is expected, since it cannot answer the prompt.
  • rig_trust_policy.py LIVE (comb, wiki, store on /SD; fenced 30d3c2cf, unfenced 84f89fda): both reached "EMPTY + NEW HOST" with "CONNECTION CLOSED" on screen. Both then died in the rig on the same harness error: the 'Q' writemem "connection dropped", which this stock firmware does often (below). Inconclusive for the policy's later phases, on both arms alike.
  • Many small transactions: robots.txt on comb, 6 more per arm (DCCDDCCDDCCD; 94f0f3a4 / 67f9d2d4), so 9 per arm with the A/B above.
    • Pooled over all 9 runs per arm: fenced median 39.73 s (39.35-41.86), unfenced 33.24 s (32.31-34.39).
    • All PASS, byte-equal, $00.
  • Across every C64U run above, net_last_error was $00 on every fetch, and no $86 / $88 / $89 appeared.

U64E: 3.15 git 3a1ff9ff, FPGA 126, core 1.50. Built from the pre-rebase tree (71b047e base + this PR's two commits).

Why that is still a valid measurement of this change:

48 MHz, same protocol, n=3 per cell:

page build fenced unfenced
/robots.txt onchip (5d8f1447 / 7204e616) 64.55 (63.91-64.99) 54.88 (54.48-55.60)
/robots.txt comb (f6d67a27 / a980b21d) 52.92 (52.40-53.00) 44.30 (43.01-44.65)
/wiki/Commodore_64 751,823 B onchip 375.45 (375.40-376.94) 175.28 (174.90-175.60)
/wiki/Commodore_64 comb 363.89 (363.54-364.04) 163.29 (163.27-163.42)
  • All 24 attempts: HTTP 200, $00, overflow 0, and complete by framing.
  • robots.txt: 12/12 byte-equal to the host fetch.
  • Wiki: 11/12 CRC 9C298DA4, metadata-only against the host reference. The 12th (onchip fenced, 07:23:36) was a 752,043 B experiment render, served chunked and complete by its terminal chunk. It is DIFFERENT only against the host's cached render.

Local listener on the U64E (HTTPS_SNI=www.foo.invalid builds, DEBUG_CAPTURE=0, 71b047e base):

  • 48 MHz onchip (f737e13a / cab45227): 6/6 PASS.
  • 48 MHz comb (5c03dee8 / 6b6e0390): 6/6 PASS.
  • 1 MHz onchip, n=1 per arm: unfenced (cab45227) PASS, sentinel at 2,235.3 s, $00.
  • The 1 MHz fenced run is withdrawn as evidence (adv-299 finding 3). A helper script rebuilt build/ while it was in flight. The rig loads build/ by path and, at the time, logged no hash of what it loaded, and the labels are identical across arms, so what ran cannot be proven.
  • rig_https_local.py now prints the sha256 of the PRG bytes it actually loads. A 1 MHz rerun can follow after the U64E's firmware window.
  • The real server cannot be tested at 1 MHz: the handshake outlasts the server's patience.

Not covered

  • A full fetch through the shipped store images. On the C64U they stop at the NOPATH prompt, because the device has no /USB1. That was run, and it is clean and identical across arms; the store itself was exercised with /SD builds instead.
  • The U64E store path (/USB1) waits for the end of the U64E firmware window.
  • rig_https_local cannot pass on any store build: its trampoline never runs trust_pre, so the store refuses the certificate ($04), on master too. The local-listener runs therefore use no-store builds, which differ from the products only in TRUST_STORE.
  • $DF1C bit 3: there is no in-PRG counter on this backend. The evidence rests on net_last_error, where a refused push lands as $86: it was $00 on every fetch. (My scratch rig also logged the UCI status line, but it over-read the 16 B uci_status_buf, so those logs are not evidence and are not cited.)

🤖 Generated with Claude Code

@JC-000
JC-000 marked this pull request as draft October 14, 2026 15:53
JC-000 and others added 2 commits October 14, 2026 10:53
…ores them

The plain UCI adapter put a uci_fence after every register access. Reads
need none (the queues advance on the read, DATA_AV/STAT_AV are polled
before every byte); measured on the U64E with the fenced 6510 comb build,
read fences were 200 s of 363.6 s on the 751,823 B wiki page (#296's
fairness measurement). Every WRITE fence stays, and so do the push rule,
the shared uci_fence macro and every TOD-bounded wait.

The 15 read sites in net.s (5) and uci_cmd.s (10) now use
uci_read_fence (or an .ifdef around their jsr uci_settle). The trust
store's uci_dos.s reads are left fenced: that path cannot be exercised
on the C64U (no SD card), so it is not changed here.

UCI_READ_FENCE=1 (BACKEND=uci only, in flags.stamp) assembles the
previous images byte for byte: uci-onchip 375f9480, uci-comb 74a7d358,
plain uci b4e4e6fb. uci-m3 (44648860) and ip65 are unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
tools/check_uci_read_pacing.py runs the shipped adapter (net_init,
connect, data / idle / EOF / reset polls, send, close) on
test_uci_abort_recovery's interpreter and model, and logs every
$DF1C-$DF1F access with the uci_fence executions after it: every write
must be fenced; on the default image no read is, except the fence net_poll
keeps before its header read (now labelled uci_poll_stage_fence; a label,
no bytes); on a UCI_READ_FENCE=1 image every read is. --expect flips the
expectation to show it red on the other image.

tools/mutate_uci_read_pacing.sh: 5 source mutants (two write fences
dropped, three read fences re-added); all killed, control 6/6.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@JC-000
JC-000 force-pushed the perf/uci-read-fence branch from 7215316 to fa685cb Compare October 14, 2026 15:55
@JC-000
JC-000 marked this pull request as ready for review October 14, 2026 17:38
JC-000 and others added 2 commits October 14, 2026 12:42
…nts PRG sha256

adv-299 finding 1: uci_dos.s keeps the fence after its own three reads,
but every DOS transaction also runs through the primitives this branch
unfenced (uci_wait_idle, uci_wait_reply, uci_check_err, uci_drain_resp,
uci_drain_status). So on the default image the trust store's traffic
(trust_pre on every 'G' in both shipped products) has unfenced reads too.

check_uci_read_pacing.py now runs the store's DOS traffic on a
TRUST_STORE=1 image (load on an empty medium, stage + save, reload; on
test_trust_store_6502.py's DOS model) and holds it to the same rule: no
read fenced on the default image except uci_dos.s's own three, which are
labelled (uci_dos_rd_part / _data / _state; labels, no bytes) and must stay
fenced on every image. mutate_uci_read_pacing.sh builds the onchip store
image and adds dos-own-unfenced and check-err-fenced: 7/7 killed.

rig_https_local.py prints the sha256 of the PRG bytes it loads (adv-299
finding 3: a rebuild of build/ during a run must not be mistakable for
what ran).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…dos count

adv-299 round 2 (low): no flow executed the data read at uci_cmd.s
@drn_have, so re-fencing it on the default image or unfencing it on a
UCI_READ_FENCE=1 image went unseen. New flow "poll overclaim": a 600 B
header over a 512 B request, so the copy stops at the request and
uci_drain_resp reads the rest. mutate_uci_read_pacing.sh adds
drain-resp-fenced (killed: 49 fenced reads in that flow); the reverse,
the site unfenced on the fenced image, is red too (49 unfenced reads).
The docstring said uci_dos.s keeps four fenced reads; it is three
(DOS_OWN).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@JC-000
JC-000 merged commit daaef33 into master Oct 14, 2026
@JC-000
JC-000 deleted the perf/uci-read-fence branch October 14, 2026 20:07
@JC-000

JC-000 commented Oct 15, 2026

Copy link
Copy Markdown
Owner Author

Post-merge confirmation on master daaef33, U64E fw 3.15 git 3a1ff9ff, FPGA 126, core 1.50 (read through the harness before the first run; storage volumes Flash, Temp, USB1, with USB1 writable). This closes the two items #299 had deferred to after the firmware window. Each run took its own DeviceLock; F = UCI_READ_FENCE=1, U = this PR's default. None of these runs is pooled with any run from before the window.

1. 1 MHz, onchip, local listener (rig_https_local.py, HTTPS_SNI=www.foo.invalid no-store builds, DEBUG_CAPTURE=0). The rig now prints the sha256 of the bytes it loads, and it matched the build both times. n=1 per arm.

arm PRG result sentinel
F a665e6f9081e64ce313ec173dcc1745c654ff2d90b272d62cf50408a291af96d PASS, net_last_error $00 2,312.9 s
U 509a247ce7dab1d8eddaec3de7b2613f8046b411a562ac6bc1922fb6874382f4 PASS, net_last_error $00 2,207.1 s

This hash-logged fenced run replaces the pre-window 1 MHz fenced row, which was withdrawn from the PR.

2. Trust-store path on the U64E's /USB1 (48 MHz, store at /USB1/c64https-test):

rig build F U result
rig_trust_store.py onchip TRUST_STORE=1 8115624e4c76d1222b6caff07efd6e89003960a80c5d1e6ece68f6abcbf4a802 1ed6318d88d2daf986576eb57f68d80f68c8fe29553be20d70e1870c7a1346b8 24/24 checks each
rig_trust_store.py comb TRUST_STORE=1 f5d112a17c21e6dfa4bb17c23c2634593a8c372e474e2a3e46e62aa4fe8ad238 76db68462b4da9e88726e0a63f2425ad3967d985b56952957ade4426f86363ea 25/25 checks each
rig_trust_policy.py LIVE comb, wiki, REU sink a52b8a005b57e0df14a0bce53ae73beec9313fce08abcb348ae5a7478c468baf 4cf3c2c3429b4f9707439a99d63b74bb884bfb50f0becee93574bb655f29f66c 4/4 each
  • The store runs cover load EMPTY, save, VALID, ping-pong, a torn write plus reset, FORMAT fallback, repair, CHECKSUM, VERSION, 32 records (a 2,064 B multi-part image), FULL and NOPATH. The 25th check exists only on comb (the cold-bank image in cert_buf).
  • Trust policy, both arms: EMPTY + NEW HOST, KEY RECORDED, settled, and TRUST.A holding en.wikipedia.org's TOFU record, whose SPKI matches what the host sees (ce88c808…). Rig wall time: F 436 s, U 233 s.

All 8 runs passed.

🤖 Generated with Claude Code

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