Skip to content

Intermittent UCI handshake stall after a good connect: tls_state=3, read_seq=0, ~707 B consumed, ring empty, net_last_error=$00 #242

Closed

Description

@JC-000

Seen twice while measuring PR #238 and PR #241 on the U64E at 48 MHz. Cause not established.

Signature (identical in both runs)

Runs

Lane Image Run
#238 A/B, B arm `97f1aa98…` (#238, before CLR_ERR) B3, port 4433, trampoline ABORT before connect
#241 A/B, B arm `587b068a…` (#241 on #238) run 3, port 443

That is 2 of 27 runs on images carrying #238's change, against 0 of 21 control runs. One-sided Fisher p ≈ 0.31, so not evidence that #238 causes it.

Not the known REU trap

Both images are onchip (no REU row-fetch DMA), so CLAUDE.md's "REU-profile build on a REU-less device" cause of the same screen does not apply. `net_last_error=$00` rules out a `$86` wedge.

Next steps

  • Once tls: fail closed on AEAD tag failure instead of polling on (#239) #240 lands, a repeat should fail closed with sub-progress `$0B` (AEAD) or `$0C` (framing) instead of hanging, which says which one it is.
  • Capture `tls_rec_buf`, the handshake keys, and the transcript hash at the stall, and check them against a host-side decrypt of the captured ring bytes.
  • Candidates, none measured: a transcript mismatch (wrong bytes folded into the hash, e.g. a CCS or a duplicate), a corrupted key derivation, or one byte flipped in the ring.

🤖 Generated with Claude Code

Activity

  1. JC-000 commented on Sep 24, 2026

    @JC-000
    OwnerAuthor

    Update from the PR #241 adversarial review (ring dump parsed, not re-run). In the #241 B run, the ring holds the entire server flight, and all of it was consumed: SH 95 + CCS 6 + EE 55 + Cert 392 + CV 101 + Finished 58 (header 17 03 03 00 35) = 707 B, with head == tail. The client stalled right after consuming the Finished record's header, with sub_progress=$02 where passing runs reach $0A. #238's B3 shows the same state, with a certificate 1 B shorter (706 B). So this is a client-side stall in the deframer/TLS path after the whole flight arrived, not network loss and not a connect/abort problem. That points away from net_poll and toward tls_deframe.s and the handshake state machine.

  2. JC-000 commented on Sep 24, 2026

    @JC-000
    OwnerAuthor

    Another occurrence, on the shipped menu path (do_https_get), captured

    A third hit, on a different image and a different path from the two in the issue body. Hardware-runner lane, 2026-09-24 07:40 CDT, U64E 48 MHz.

    • Image: PR fix(uci): wait for ABORT's reset; abort on every connect/close timeout (#230 a, #221) [stacked on #238] #241 head 3677c4a, make BACKEND=uci USE_NISTCURVES_ONCHIP=1 HTTPS_HOST=en.wikipedia.org HTTPS_PATH=/wiki/Commodore_64, PRG sha256 2596bd347380d56d3f976ace96c0f26d6a983e2461de2f01e6bb8d00053aa2d4. This is onchip, with no REU used (LIB_NISTCURVES_REU_BANKS_USED=0, device REU Disabled).
    • Path: tools/uci/rig_https_banner.py (menu 'I', then 'G'). It went through the real do_https_get, not the trampoline, against the real en.wikipedia.org:443. That was run 1 of 5; runs 2-5 on the same image completed 751,225 B bodies.
    • Screen: HTTPS GET EN.WIKIPEDIA.ORG... / DNS OK / TCP CONNECTED / CH / SH / HK1 / KEYS / ENC1 / RX / TLS HANDSHAKE FAILED.

    Timeline (from net_tcp_state reads, an observation wrapper around the unmodified rig)

    Captured state (read-only DMA reads at 07:49, after the failure; $A000 proven RAM: 5a5a6d84…, not ROM)

    symbol addr value
    tls_state $A24D $FF
    tls_last_state $A24E $03 (waiting for EncryptedExtensions)
    tls_read_seq $A40B 00 00 00 00 00 00 00 00
    tls_recv_progress / _sub_progress $A24F/$A250 $05 / $02
    tcp_recv_head / tail $A244/$A246 $0233 / $0233 (ring empty)
    tcp_recv_overflow $A248 $00
    net_last_error $B3BE $00
    net_tcp_state $B3BF $00 CLOSED
    tls_rec_header $A413 17 03 03 00 35
    tls_recv_state / count $174C/$174D $00 / $0000
    df_last_err $3B79 $00
    df_mode, df_hdr, df_hdr_have, df_msg_rem, df_rec_off $3B66… 00, 00000000, 00, 0000, 005a (LE $5A00)
    uci_socket_id $6852 $07

    How this differs from the issue body: here the client consumed the whole server flight, well past 707 B. The 4 KB ring at $C000 has wrapped: the head is at $0233, so at least 4,096 + 563 B arrived. Walking record headers backwards from the head gives a clean chain ending exactly at the head:

    $C2B6 17 len=529   x7 (…$CF3A), wrapping to
    $C150 17 len=63
    $C194 17 len=96
    $C1F9 17 len=53    <- last record; its 53 B payload is what sits in tls_rec_buf ($A000 = ring $C1FE…$C232)
    

    53 B is the size of a Finished record (32 + 1 + 4 + 16). The SH/CCS/EE bytes at the start of the flight were overwritten when the ring wrapped. So tls_read_seq stayed at 0 while every encrypted record of the flight was read out of the ring. No record ever decrypted, and the state machine never left WAIT_EE. That fits "the first encrypted record fails AEAD, and on a pre-#240 image the failure is retried as incomplete", with the retry walking on through the rest of the flight. It does not fit a missing read. #241 does not include #240, so this image still has the silent-retry behaviour.

    Full buffers follow (hex, 32 B per line). The ring file's sha256 is 528969de8fbd4a9485331b912113ab2fe0d79f5a60fc97b1824ce7636cf63dfa. No handshake keys or transcript hash were captured, because I did not have their labels in this pass.

    tcp_recv_buf $C000-$CFFF (4,096 B)
    36decc0cdd232dc2573366d46e868eda3b5c08c6230b9b24e945e34754816498
    7489eecbca0a53239ba9cad2e16b4f13e287216281fe7448b8381851f4e98b92
    7bab9a1c68d8a9ff6cf2cef4f1b76fc9bcc15c1852351047a578ff02b3638a42
    8d9ecc875df4004a897d8de46899ebff9b70eede8b160e9ea3e4e94ec49b3c1c
    261fd05ccc75579d14963966902737b9ed560e44bf851da08960e059e219c7a1
    4fac594fd1baba3ae1c3c729aed179774c96d424ae481f93db820a98f8674d09
    6df0f6a98b4ef392288578a4bbbf6cad9779725531c6fd93dfff081f1493f321
    c81e50ddcce3e23e2c129b4721c687e95e10a2c5ace42fbe550035f08ee23090
    e2496f4d94b7cc19bc7ab504738c92c8a256ad2ba7c8573aa181b7423eda2a30
    6247886ba1ca481280d95ead456e235fc376ae096ae8ebd66280124a41c06443
    c3df0756d4d7db0ea7edc292fdf44b61170303003f347c911befb8f625aafb0c
    9aff1074a9d4852d9aced2838299c0cbdaad2fe62642f29c74b2eeaff3302876
    7453a3c6b1fbae92742f200c901992ef699de9b9170303006040d6072aea70ee
    d35aa04674283c25a672d1297870d43486e0ef18ccbb3b92d57122db56b2cda2
    415e6325bd43aaa8526a3a5ebe9a4c78ac6d1faff60442f66616b254ce4869e3
    13d6912002bb40ef67f600e248f4e60d2a31fc9248bff8f51f17030300355a5a
    6d8405310681cedbd4b5e41a73ffe6a0bcfb143c51e872e741594b4e57f5fb4a
    b81355cc629011b4a0ce0ef1b615e28b1e040c907abfe14f66fab3aaa5afd4ff
    7f13cf7dc7dc0c888996887ab81def9624fd22a5c781c47c1de98aae19c0d2f2
    56d927c42074d52ee74353ab1d81a33540cf86e58a484b70c8ab046fcd103511
    d170a98a8f2c7f086264c96516972f81f8c2d81a4edd15c5ed1ea8dedb158e73
    b6bde5a175a7863b4ddbe9066c2f2cc7f0364558eeaa17030302115973a5439e
    82d50695f9a7a3e7f6f854e14333098cf4489589962437abf2eea27e0d7a2437
    a2931a91e8180a71a71de7da4b5a750f345ad069ba8a404541aa45a3dd6ad3ee
    83454de268d3c714d5ba163c975204624c14b1c47e6c3380cd8b426da68945b4
    0b0464b747086f7838b5c17c6aaed74c204c681d4869701e0f0dafbe1fcd7760
    ed82f57d8ef596a47a76bd6ca435b81837f59f29044665b0455d1caa134d4477
    2914b94dd49b013be67be0c9783f1ca0ff342e4030a9ab3da52cc405fa06b1ba
    d824d80364be1604e0a1e1343f124546e4afc1b68a852ba8dd8b35ce5c7f481d
    4b47110ae545fa2e1badcd09f4a736aedee882770a8dc1ef3c5f7f584b078d70
    0940c7213fbc5a2cf695776ce9f68a500c68ff37766d8207893f5c29c8e75a22
    dffc8e357400858b3f15243680fe9fe355645c84a7ca800644511b2c1553e862
    1e48be619033a73d191580e36712dd480a2e306a8f5a3e1c58294805a5311f7d
    33e437e3bf4f4ef9d497ec9694636095438601b72c2f186d7b8909ab255bec7e
    1c91edf7a657da5736c450571d882622fd8ee340a7bc231c71e227a006b7f6b5
    1be2ebae729d9849d922bc74cf2730a7b5e52a896a1ca7d05ec16105cfb45a8c
    9c5247a7392e4f5c9e8d37bbffd589bc225d47893b9a8e9ebe64b40a813c05c0
    41cdaa8add35ecaeb7bb331f1297446b0fda01024f787d1aff021945f2dfb8a2
    bb1b7956f5331802513bded0170303021170a39aaf625a161070cb5fb325f8f5
    51886939246b3a398d5ec451a45657352760ba202a16ff617aec55f2b92b0e9e
    033e91647ebebd4f8567faca1748730745786a6a5f7ce218d8b4047fa7f51c0e
    49d62b5418bf2e3ee2e534773b2f5da25798309c0c15572254d0f20617d29aad
    af06ed28bd6bf316cab0221f1e66ef73673016747ab34ff376af1ef1b776e208
    1fdd2ab38d226c2f1112d9bea5d0500112c1791095ca050753a5074ace59bf27
    0b2e40d9cf1bb0afe5a51ee654b9687b55175a25b0b514381222f6c6e12b9f1a
    43f1056ba619badfa740cf40d9e576fdb206cd3f9ecc9597bb7553d6e26fb112
    778d80294fb80eeaa70c07d173d62963da7b9d5e313cd821c95e0e44085e9975
    94677a02262991c0f521ee3eb4737e5595b0742e2ca492f3ee1acd97cf3eb1bb
    424366b5b1b72233b9f99f97e958f801bf0d41195f85ca8a5c889b64f6eecfcb
    b1fdc5351c66f61d41b55e7572b01e2c4c107b76b25f29c7e9df54bdea84ebff
    ddad46434975e3dac336d6bdc0e9581dc54c28d929947230575664b66722cb4f
    dbca44ad09e777695d20f5d55ba75a1ad4f7cefc52a6c50c371eb3c13ee83559
    e2983e18a66a3ebf9a8cc19e27319d47dd262a0c8074be7e7cf28d54ed85374a
    4cc89f1fcbc07c738245e0e42660bec0c2f9c3eb68b942b031f52bbbd8c24cc2
    6b5bc2ba3a816c7499ab7916276957817902d6f2d1c64ce4866060e31d013509
    19831703030211f55bb72b4908825d734baf62be073fea507c00f4bb41396544
    d9ef33b9645748dcb5e41212f18ec86d4b39c3974baf86d9fe038c86b7db38ea
    07fe88b5dec0f62024039835f99d7d8b17d52e8cb6bffb723e9f36427963dc8e
    55e6d007aa69c2ce03ac51df3d84396b94fbc50077e5b8708493ba74e59d33a2
    bfa80ed51115d0e060f46cd390156eb489fccd8c233d0fd53f00e65a857655c5
    5c371ebe2d95c93981ea0288275abbc421b8c64addf6ef0669e87b16914a241f
    30e47b90c2e5064e3317b6188cc2bf1314972b65bf9ee19a5d680f917d532300
    837e1f2cd4edf7abb12d86758b80ec4328e5bca06ce980ef3814c385654a713e
    7e033c6c5b00c327d9aed5caa5fbeddbd3920671b3fcb92484d0010f7714657b
    f06ab2dabb8b21ff366293bab98ef99c6280d77af32b7d0711537020bf51c17a
    c19bd701ca05443e99f8c0c9595eba93a7796c595aeb2f92960d8bf447780d40
    dbb187c0b424a703ab3b864143178acb163e04b608f2dba4d419ff6eed5283e5
    59f0173b4fbdc32e41c39df95c8d5de5a840f3b0019f0b3d6ed195a8007c5213
    b1958f13dec818461694a8d3186f1071d1b1e07035912b154c4a8c6c132de6c5
    1f036bde264758c64d2507d03a435e790cdc77a6c21b589c324f9ffb21190099
    578e4bb74b8885787c10eed5dab9f67e9635f3ccfe6edfc64cf8a5dcf9118e5e
    08c291adb708840952018f246b7b8981f6dd73153431b1361703030211f5a8ce
    c771ee76dda863f66b15d633015ce0fff1af0cd991ca85f3d5e3b376a42ec93b
    e509348009336e083ddc71fe528c9f7a60a33bd405dd7891997cf7e37bdf0785
    d385a772a123aaf76e129df5391031999b71887afb742e4f899345d7dfb3fe74
    3496b6ff7b88330bc109244ecc083bfe9c7cd3a3092409843ee80923f79b6c5d
    7605fa174510c646400a579962e6828ce9cc97571caf4180dca00b34c564a55a
    686270f6288b5947788281c790d3cc5d026289bd4fe59e1845b65b17bff7e602
    e0eb6ffa54a833cb822c3f4fb9e6e4623fb8026985abc5b153058897b176d801
    db6d85df07299f987780ea19855795a47f08ed4d2432b74dd846109be84a10c6
    4473ffbf17e721df805a542a851c94f3039ad97060f752317c830dc929e0531f
    ef41b04679a83308b3557f90c6b9f8cd7d6c47f9a87d981e116dea37ee55b44a
    ec6dfa020bbf8e52608d3570bcd770b18340d90c6414d6c15c1fec6a44c1ceec
    39b320ef6bba56ccd1dc1d48f7e5586e6a8a4c71add07d77188e6ab1eaef9c7b
    1bf7d1d493189ef3e8013b63da978a279944317bee1f8b7753333efba789bd50
    317e5d15d5c2b390bb533e9f5c68267157e3149731bc4c14d2128d2acc533dec
    07f8737e37c512f3a3213915bae40be2cf8633d203d5b7df352d8a74b54c5985
    ef0dc259a664fbe027f2df9b9d454df872fc30a200402ce6e85b983851c1bae9
    dc7fac224e57ddab08b033a7b7721703030211e28110ec48729bfd9c21136b22
    b0a1fd4a9f92fc2940d0d76fd04f276a5fbd66151ccb10673587a7ea146a83da
    8f039f68f9622538cc37b8a8670ec260eff1e2a52a4559ba60932dca9598a1c0
    a2ff2bd7500f68522f2f95ff04bf551dfd0541fce36e37abc7b828993e665093
    b545675bd24f17d742929117a2fe4b094c7da59f7f961d59cd2541ed95014e85
    fbf1558886ce6daac3a898476a82c592ff59a61791b2fcc8c6274e70ae5fa3bb
    89ca15a608d5be1043b97b9bc623c784f9a09138bc74541be2700d87a5df3b3c
    2072bd2fb39361eec686dc5fcb8fa823d2c04e76536c8de1471f60a2685ecbd5
    3227f27c8456770314a2ff252ca53e39db6e33fe2b126ca2c6bde50f564089c9
    55da7fa3a94432192a8bf8b26a467397e9596797f6c20a0f0b4b64059c8ad323
    3dc607d80c1590ff0b0122b0bd94bc1bdcbc28fa4001019557e72dcbfceeab45
    30be054c579786afa13a5d8b7c91a071cc63e73aaa1343acce9d7dd74d14b66f
    a350579d7926334ec7efd3bc0141a586604e37ebdb34f4d799e577e21822c299
    67c3d0464b2e3de82e68e748555007c52bb237459c9f0ecc07ad0761bb224a16
    9d4f6ccf67ebdd9f0e2e274025bd2237c674a3af3b75e171bdfcb0c9f5562a3e
    6e0e15bf0344f67c40e04f578e4756de1a4e2263509f3f4f817ea619d1e1df09
    3b5fe92cfee33045c872a726707a51ec3b8f126b0f626d8ef2771c5165c889c9
    5ae34e8217030302115b2bfb34a6957c863c2a4893be1587ce4e030a7e8f0269
    c1bce1ddf468e28b4666f1e88159b2d0270832c83cfed15147ed3e1b99054997
    19a344f4a3f8f5738695c40ea038ada76d6056b21c556029a06d62ba112a4f3e
    560df9cb62f66734720ee6fd2058a3d04a8962c2ecb2d0d90ccbe32520b12cb5
    934e42e9a0fec7b8e9735e3473b5e1fccdbdaa6991dac1e90232f80a7c741966
    feb89f4c8d911719e99d3259755285becde4a4db3c8978a2168b6a257627ff2d
    798f56afdc7a74d41c6b9a299d6807e30bde4f69833542d465f19ff3f60a4ad1
    fe06e2b842c8cc6af3af7fd1dae8442f69617fecb06dd7357ae4dc8eb2b24bd0
    a0f5e99a81acff6409fa536b247284ae83ab747902f6775920b503a9b227b9ab
    3656289c781dae186a30d2a1de2268e09a55fa91ee0a524e51775ae95a98c71d
    805bb2caece95e9d37cd044d2d73678cbe9f5c594cde351f6048acccbf4698e5
    67656c582ae87a4ece7e3bf708cbe254f11b674d4832a5d4b8b972392cc9fcda
    32628cf9c2307edab628450936e3c41824f4b102b2ca3a934e5445f83add39f1
    4531d5adb3b896cf197566d02c2557f40d641aa82e370f77b760f0b6b65c87c7
    6dcaf26bc7d3f89a37d88f2cc459efff5cc0fc8979cad02315cb67c6c02cf9d6
    ce6b604a991abc503fe8632ef227b0d03c06c388d606ba27a763de6570c1aef1
    7b33f372d6e7b5bd026deaf1b07dfc5593ea904487888c628c24170303021182
    1494e6383e42587fbf2454e632212baa9a56f837efb8cf57fd38623ebb348f49
    c6359b08423efe47ab6a696d18e19df8513155da4ba01bcc26103b88832930ef
    bd11a5703ce81e086200de4aeb5a530fa368281bf1d947b3fe32478692e4aeee
    a92286a2d7abad26dd24fe2fc4d1bb496d44b2ce6388c1e5304cbbb72ba8ea10
    52a885398778cec891bd0e3b8c77bea073cacce8f068fb033dd0e8db64d1acf8
    cfd0a1ba510941ee96b0a70203e846157463469f4b2196bcdce07f7dff6f7f47
    
    tls_rec_buf $A000 (548 B)
    5a5a6d8405310681cedbd4b5e41a73ffe6a0bcfb143c51e872e741594b4e57f5
    fb4ab81355cc629011b4a0ce0ef1b615e28b1e040c4c78ac6d1faff60442f666
    16b254ce4869e313d6912002bb40ef67f600e248f4e60d2a31fc9248bff8f51f
    eea92286a2d7abad26dd24fe2fc4d1bb496d44b2ce6388c1e5304cbbb72ba8ea
    1052a885398778cec891bd0e3b8c77bea073cacce8f068fb033dd0e8db64d1ac
    f8cfd0a1ba510941ee96b0a70203e846157463469f4b2196bcdce07f7dff6f7f
    4736decc0cdd232dc2573366d46e868eda3b5c08c6230b9b24e945e347548164
    987489eecbca0a53239ba9cad2e16b4f13e287216281fe7448b8381851f4e98b
    927bab9a1c68d8a9ff6cf2cef4f1b76fc9bcc15c1852351047a578ff02b3638a
    428d9ecc875df4004a897d8de46899ebff9b70eede8b160e9ea3e4e94ec49b3c
    1c261fd05ccc75579d14963966902737b9ed560e44bf851da08960e059e219c7
    a14fac594fd1baba3ae1c3c729aed179774c96d424ae481f93db820a98f8674d
    096df0f6a98b4ef392288578a4bbbf6cad9779725531c6fd93dfff081f1493f3
    21c81e50ddcce3e23e2c129b4721c687e95e10a2c5ace42fbe550035f08ee230
    90e2496f4d94b7cc19bc7ab504738c92c8a256ad2ba7c8573aa181b7423eda2a
    306247886ba1ca481280d95ead456e235fc376ae096ae8ebd66280124a41c064
    43c3df0756d4d7db0ea7edc292fdf44b61000000000000000000000000000000
    00000000
    
    $A200-$A4FF (ring pointers, tls_state, tls_read_seq, tls_rec_header ...)
    43c3df0756d4d7db0ea7edc292fdf44b61000000000000000000000000000000
    0000000000000001000000000000000000000000000000000000000000000000
    00000000330233020000000000ff0305020300c84be4ed25943488d77a9c16f4
    8a0283a4d6cf411779cc72f36599f5413b0216c5a284b837651660f866fc9266
    d59120fc0d364843d87e90e4965b50c9c45e976070a17ca41b28c9a767bbcb5c
    09984db799efe30fcdd27b772c4c5273d854caeca6f47c0dac9595f0d33a5cac
    283324122155b610cbf8bde7af3852d0603a12c184909e5fd3d4f26a9f5264f6
    506bd48032a66663af7813dbc54dd6c760447e3f2b91be3c4459f50e79d023ab
    5a315a74a895dd110e186b926ecdbb4c3e6b7f96214b07c03a80285c26e2e6c5
    85a35e8ce922e46853bb20259eaa7ccf0f84ed2dcdce92e37dc285128d8c6aad
    d0f289b02e31197cba58c6306f3f7761e89efaa16c22617736e604dae8947dd4
    4e62ed6fbca6192d763df1324a31f9e9ce964a2365fcbf83097ad5a89b41ec49
    a81cd6dd69c4ca80aab5c2fffe8a80f39f4621648f7eb198aed627da6d3d15cf
    cfa15b219bfdde3d5ce00c000000000000000000000000000000000000000000
    0000000000000000000000000000000000000000000000000000000000000000
    0000000000000000000000000000000000000000000000000000000000000000
    000000000000000000000000000000000000001703030035173500cfcfa15b21
    9bfdde3d5ce00c22e89cd1b23b0bd0009b82336f2e26db23dd27f868ec3c80e9
    31c893b356a9d9cfcfa15b219bfdde3d5ce00cfffe8a80f39f4621648f7eb198
    aed627da6d3d15000c08746c733133206976000020747261666669632096214b
    07c03a80285c26e2e6c585a35e8ce922e46853bb20259eaa7ccf0f84ed000000
    00000000000000000000000000000000000000000000000cecb220f3a2202c67
    023667000c33ad0a1c607ec03b09e6cd9893680ce210adf300aa1f2660e1b22e
    10f170f92ae21eaf292922b6a4fa7f390cc6ea7ead9db049146879c31c5cf1d9
    
    deframer state $3B66 (64 B)
    00000000000000000000005a0000000000000000000000000000000000000000
    0000000000000000000000000000000000000000000000000000000000000000
    

    🤖 Generated with Claude Code

  3. JC-000 commented on Sep 24, 2026

    @JC-000
    OwnerAuthor

    Root cause found offline: an X25519 field-arithmetic carry bug that sends the wrong key_share

    The handshake keys are fine and the deframer is fine. The ClientHello's X25519 public key does not match the private key. The cause is a dropped carry in fe_reduce_wide (src/crypto/fe25519.s, grep @prop2:). It has been in that file since the first handshake commit, so it is on master and in all three shipped products. #238 and #241 do not cause it. The fix is in PR #244.

    From hw-runner's capture (PRG 2596bd34…, which I rebuilt byte-identically from #241 head 3677c4a)

    • tls_ecdhe_pubkey = eca6f47c…, but X25519(tls_ecdhe_privkey, 9) = c689799c…. Mismatch.
    • tls_shared_secret = X25519(priv, tls_server_pubkey). Correct.
    • Host HKDF over (shared secret, tls_transcript) reproduces the captured early secret, handshake secret, and all four handshake key/IV buffers. The key schedule is correct.
    • SHA-256(ClientHello || ServerHello) equals the captured tls_transcript only when the ClientHello carries eca6f47c…. That is the key that went on the wire.
    • None of the 10 records left in the ring ($C150 63, $C194 96, $C1F9 53, 7× 529) decrypts under the captured server handshake key at any sequence number from 0 to 63.

    So the server derived its keys from a public key the client holds no private key for. EncryptedExtensions fails its tag. On this pre-#240 image, the failure is retried as "incomplete", and tls_recv_record has already reset its state. The client walks the rest of the flight, every record failing with read_seq stuck at 0. That is exactly the tls_state=3 / sub_progress=$02 / empty-ring signature.

    Reproduced on the real PRG in VICE

    • tls_ecdh_generate_keypair on the captured private key, in image 2596bd34…, outputs exactly eca6f47c…. The failure is deterministic and depends on the scalar. There is no timing, network or hardware component.
    • I stepped the ladder op by op, checking each field op mod p. Exactly one op is wrong: fe_mul at ladder bit 110 (CB = C*B), with got …225259000064 against expected …22525a000064. The carry was lost past byte 2.
    • The mechanism: @prop2 ripples with lda / adc #0 / sta / bcc / inx / cpx #32 / bcc. cpx rewrites C, so from byte 3 on the loop adds 0. @prop3 has the same defect. c64-x25519 found and fixed this pattern after its v0.1.0; the in-tree copy never received the fix.
    • A byte-faithful Python model of fe25519.s with the bug reproduces eca6f47c… bit-exactly. Across 2,000 random keygens it gives 1.3% wrong, so about 2.6% per handshake, since keygen and the shared secret are both exposed. The field shows ~3 in 51 runs; these are compatible, but n is small. I checked the model against the hardware code: 6 scalars it flags, plus the captured one, are 0/7 correct on unfixed master uci-onchip (fcbd8d19…) and 7/7 with the fix (8bb53057…).

    What this settles

    Reproducer

    tools/test_x25519.py (on the fix branch) has two new groups that run by default: fe_reduce_wide carry (#242) and x25519 #242 captured keygen. On master they fail 4 of 77, and the keygen prints "= the key the U64E sent in #242". With the fix they pass 77/77, on ip65 and on uci-onchip.

    #240 is still worth landing. Any future key mismatch from another cause would otherwise be another ~470 s stall.

    🤖 Generated with Claude Code

  4. added 2 commits that reference this issue on Sep 24, 2026
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