Skip to content

boot: 'Q' scrubs session secrets and returns to a working BASIC - #282

Merged
JC-000 merged 5 commits into
fix/drbg-own-keyfrom
fix/quit-clean-basic-exit
Oct 6, 2026
Merged

JC-000 merged 5 commits into
fix/drbg-own-keyfrom
fix/quit-clean-basic-exit

Conversation

@JC-000

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

Copy link
Copy Markdown
Owner

Merge after #279. This PR's base is fix/drbg-own-key, so it shows only its own commits.

Defect

'Q' banked BASIC ROM back in and did a bare RTS into the SYS that started the image. BASIC then runs CHRGET ($73-$8A) off TXTPTR ($7A/$7B) and trusts CURLIN ($39/$3A) and its pointers ($2B-$38). The crypto uses all of that zero page as scratch:

  • X25519 fe_wide at $40-$7F
  • nistcurves fp_mul_i/fp_mul_j at $39/$3A
  • TLS/ECDSA slots at $02-$3F

After a handshake the 6510 executed a leftover field element as CHRGET and BASIC printed ?SYNTAX ERROR IN <garbage>; rig_https_banner ended every run that way. The session's keys also stayed in RAM after the quit.

Fix (boot.s quit_to_basic, src/session_scrub.s)

  1. Zero the session secrets in exact-size, link-asserted spans:
    • ECDHE key and shared secret
    • handshake and application keys/IVs
    • HKDF and key-schedule secrets
    • SHA/HMAC/ChaCha20/Poly1305/AEAD state
    • tls_rec_buf, which includes the X25519 scalar
    • zp_save_buf, poly_prod, mul_dma
  2. Re-seed the DRBG. This tail call is the only scrub of drbg_k, and it keeps rigs that SYS back in from drawing zeros.
  3. Zero ZP $02-$7F, $FB-$FF and the stack page.
  4. Bank BASIC ROM in and run the ROM's own INITCZ ($E3BF) and NEW ($A644), then READY via the warm-start vector ($A002). INITV is not called, so cartridge/DOS-wedge vectors stay hooked.
  5. The screen is kept.

After 'Q', BASIC owns $0801-$9FFF, which is the image. SYS re-entry is valid only before BASIC allocates anything; the rigs only type SYS. uci-m3 gets the ZP/stack/BASIC half only.

Evidence

tools/test_quit_basic_exit.py (VICE, 68 checks):

  1. Real X25519 + P-256 verify, then plant patterns in every secret buffer.
  2. Press 'Q'. At quit_scrubbed: ZP, stack and secrets are zero, and the DRBG state (including drbg_k) was replaced.
  3. At READY.: no error, CHRGET equals ROM, NEWed, PRINT 6*7 gives 42.
  4. SYS re-entry leaves the image unchanged and the crypto still correct.
  5. On a fresh boot, 'Q', then PRINT "AB"+"CD" and LOAD"P",8 from a d64 followed by RUN.
Build Result
master uci-onchip (45bea88a…) 10/22, exit 1: ?SYNTAX ERROR IN 7518.50335, CHRGET/TXTTAB clobbered, LOAD gives ?OUT OF MEMORY
uci-onchip (192157cb…) 68/68
ip65-onchip (78a10748…) 68/68

Mutants (uci-onchip), each red:

Mutant Passed
no poly_prod span 67/68
no INITCZ 59/68
no NEW 66/68
no DRBG re-seed tail call 64/68
no stack zero 67/68
no zp_save span 67/68
no mul_dma span 66/68

Hardware: U64E, fw 3.15 3a1ff9ff, FPGA 126, core 1.50, 48 MHz. Build: uci-onchip with HTTPS_HOST=en.wikipedia.org HTTPS_PATH=/robots.txt, PRG 4354278dd05d7269ba1e2360cecf69434bce7b5b4f5a2d92841dbb6fcdbb97b7.

  • rig_https_banner PASS: HTTP 200, 28,275 B equals Content-Length.
  • After 'Q':
    • READY. with no error
    • CHRGET equals ROM
    • PRINT 6*7 gives 42
    • PRINT "AB"+"CD" gives ABCD
    • with a d64 mounted on drive 8, LOAD"P",8 then RUN prints 1

Size

LOADER is down 140 B on all eight measured profiles; the tightest, uci-comb-demo-store, has 153 B free. Nothing else moves. uci-m3 MAIN is down 35 B.

PRG sha256 at 3233821:

Profile sha256
ip65 0a0b7a78…
ip65-onchip 78a10748…
uci 3edcb6a6…
uci-onchip 192157cb…
uci-comb 30cca782…
uci-m3 d2f0201c…

Stale commit messages

🤖 Generated with Claude Code

JC-000 and others added 5 commits October 4, 2026 22:02
'Q' banked BASIC ROM in and RTS'd into the SYS that started the image.
BASIC then runs CHRGET ($73-$8A) off TXTPTR ($7A/$7B) and trusts CURLIN
($39/$3A) and $2B-$38, all of which the crypto uses as scratch (X25519
fe_wide $40-$7F, nistcurves fp_mul_i/j $39/$3A, TLS/ECDSA $02-$3F). After
a handshake the 6510 executed a leftover field element as CHRGET:
"?SYNTAX ERROR IN <garbage>" at the end of every rig_https_banner run.
The session keys also stayed in RAM after the quit.

Now 'Q' (boot.s quit_to_basic):
  - session_scrub (new src/session_scrub.s, CODE): zeroes 10 spans of
    session secrets (ECDHE privkey/shared secret, hs/app keys+IVs, nonce,
    HKDF PRK/OKM, early/handshake/master secrets, traffic + finished keys,
    SHA/HMAC/DRBG/ChaCha20/Poly1305/AEAD state, tls_rec_buf incl. the
    whole X25519 scratch, zp_save_buf, poly_prod). Multi-buffer spans are
    link-asserted to their exact size. Then re-seeds the DRBG from
    SID/CIA: HKDF shares hmac_key with DRBG K, and a zeroed K/V would hand
    rigs that SYS back in all-zero "random" bytes.
  - zeroes ZP $02-$7F, $FB-$FF and the stack page (under SEI);
  - BASIC ROM in, then the cold start's INITV ($E453), INITCZ ($E3BF),
    NEW ($A644), and READY via the warm-start vector ($A002 -> $E37B).
    Screen kept. Program NEWed (the image is not re-entrant from RUN);
    SYS re-entry still works. uci-m3 gets the ZP/stack/BASIC half only.

Red (master, uci-onchip 45bea88a..., VICE, new suite):
  CHRGET after crypto: 33 E5 E5 79 07 A4 AF E3 7F 88 8B 78 1E C9 20 ...
  (ROM: E6 7A D0 02 E6 7B AD 60 EA ...), TXTTAB=$FF01, VARTAB=$0080;
  after Q: "?SYNTAX  ERROR IN 7518.50335"; PRINT 6*7 never answers;
  ECDSA re-entry hangs (1200 s). RESULTS 8/17 passed, 9 failed, exit 1.
Green (branch, VICE): uci-onchip da8a2fe7... 56/56, ip65-onchip
  7c841663... 56/56: ZP/stack/secrets zero at quit_scrubbed, DRBG
  re-seeded, READY., CHRGET == ROM, TXTTAB=$0801 VARTAB=$0803,
  PRINT 6*7 -> 42, X25519 + P-256 verify correct after SYS re-entry.
Hardware: U64E 10.43.23.83 fw 3.15 3a1ff9ff FPGA 126 core 1.50, 48 MHz,
  rig_https_banner PASS (uci-onchip, HTTPS_HOST=en.wikipedia.org
  HTTPS_PATH=/robots.txt, PRG 4d6bf67c...): HTTP 200, 28,275 B ==
  Content-Length; after 'Q': READY., CHRGET == ROM, PRINT 6*7 -> 42.

Margins: LOADER -139 B on all eight measured profiles, nothing else
moves; uci-m3 MAIN -38 B. All link incl. both ip65 wiki targets.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Amends 7d42f2e per adv-quit's review.

1. "SYS re-entry still works" was overclaimed. After NEW, BASIC owned
   $0803-$9FFF, which is the image: A=1:B=2:C=3 overwrote $080D on the
   U64E and DIM X(600) zero-filled $0803-$13DC. Now, after NEW, MEMSIZ and
   FRETOP are set to VARTAB ($0803), so a variable, a DIM, a string
   concatenation or a typed program line each give ?OUT OF MEMORY and
   leave the image untouched. CLR/RUN/NEW rebuild from MEMSIZ, so the cap
   holds until a reset. Direct-mode PRINT of numbers and SYS still work.
   The cost: until a reset, BASIC cannot run a program after 'Q'.
2. INITV ($E453) dropped: it is unnecessary and it would unhook a
   cartridge's or DOS wedge's $0300 vectors.
3. New SPAN mul_dma_lo/hi (512 B): the last multiply row, i.e. one operand
   byte, which is X25519's on a handshake that stops before CV.
4. Test: plants poly_prod and mul_dma; drbg_buf_idx is planted at 0, so
   "= 32 after Q" now proves the re-seed ran; new checks for MEMSIZ=FRETOP=
   $0803, four ?OUT OF MEMORY cases, and $0803-$9FFF byte-identical after
   them.
5. session_scrub.s "not scrubbed" list now names http_req_buf,
   http_resp_buf (the typed path stages there) and the
   HTTPS_BODY_TO_REU body.
6. CLAUDE.md: the six-line rig note became a two-line Known-issues bullet.

Red (master uci-onchip 45bea88a, VICE): 7/21 passed, exit 1.
Green (VICE): uci-onchip ac1129f3... 63/63; ip65-onchip 668efdf1... 63/63.
uci-m3 (6b3e52af...): READY., CHRGET == ROM, PRINT 6*7 -> 42.
Mutants (uci-onchip), all red: M1 no poly_prod span 62/63; M2 no INITCZ
53/63; M3 no NEW 55/62; M4 no reseed 60/63; M5 no memory cap 55/62
(2,841 B of the image overwritten, then ECDSA re-entry hangs);
M6 no stack zero 62/63; M7 no zp_save span 62/63; M8 no mul_dma span
61/63.

Hardware: U64E 10.43.23.83, fw 3.15 3a1ff9ff, FPGA 126, core 1.50, 48 MHz,
dual lock. uci-onchip, HTTPS_HOST=en.wikipedia.org HTTPS_PATH=/robots.txt,
PRG 0c92fac8...: rig_https_banner PASS (HTTP 200, 28,275 B ==
Content-Length). After 'Q': READY., CHRGET == ROM, MEMSIZ=FRETOP=$0803,
PRINT 6*7 -> 42, A=1:B=2:C=3 and DIM X(600) -> ?OUT OF MEMORY, 0 bytes
of $0803-$9FFF changed.

Margins vs master: LOADER -152 B on all eight profiles (ip65 384,
ip65-onchip 384, uci 613, uci-onchip 613, uci-comb 220, ip65-wiki 384,
ip65-onchip-wiki 384, uci-comb-demo-store 144); nothing else moves.
uci-m3 MAIN -47 B (32,700 free). PRG sha256:
  ip65              f776fbf91bd7c4219b355edcd2f3049db4c85aa7cb550fc36c17e9a8dfc60976
  ip65-onchip       668efdf12f87c256f0c504f25d50e1f753440f0ef15f755677e44815cb725013
  uci               d1c9a99606d33d7aa3783cba5afcacdd242d7e58774225a164fda35830c8091b
  uci-onchip        ac1129f3423ddb29101c8ef8c3d41ca802d8d930a77f8e5f2c9772d6ea790212
  uci-comb          8a30a691ba0d26e44039f0386d29d36f5b54c109f12f92de0c2ab3bc9e41bb74
  ip65-wiki         2a78e58c46cca557ca674a3adfb9f5b054efe9e8676669b90406f6a0d18fd320
  ip65-onchip-wiki  f8de73a4b528765b9685f00a8943cb6d1076c650c721dbd2c35239244a15488f
  demo-store        4e586b6f6bd574421ca4afebc00d6358f58ed0e1fe2691278c3f4f2c8cf8bc2f
  uci-m3            6b3e52af14807a77a6e90a6ba56259c342d83a21e787f3f04e547f1dcc2e2e9a

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Rebased onto fix/drbg-own-key (f0c878f), where the DRBG's K moved out of
hmac_key into drbg_k: private to hmac_drbg.s, unexported, and in none of
session_scrub's spans. drbg_init_entropy, the scrub's tail call,
re-instantiates K = 0 / V = 1 and updates both from SID/CIA, so it is the
only thing that clears drbg_k on 'Q'.

- session_scrub.s: the tail call is commented as the drbg_k scrub
  ("NOT redundant"); the header's DRBG paragraph no longer says HKDF
  shares K, and hmac_key is listed as the HMAC key input.
- test_quit_basic_exit.py: drbg_k (address from labels.txt) is planted
  before 'Q'; at quit_scrubbed it, hmac_val and hmac_key must be neither
  the planted pattern nor a bare instantiate value (00.. / 01..). A build
  with no drbg_k label fails that check.
- drbg_buf_idx: tls_connect now calls drbg_reseed, but no tls_connect runs
  between the plant (0) and 'Q', so "= 32 at quit_scrubbed" still proves
  the tail call ran.

Red (master uci-onchip 45bea88a, VICE): 7/21 passed, exit 1.
Green (VICE): uci-onchip 7964101b... 67/67; ip65-onchip 6fe69e79... 67/67.
Mutants (uci-onchip), all red:
  M1 no poly_prod span 66/67; M2 no INITCZ 57/67; M3 no NEW 60/66;
  M4 no drbg_init_entropy tail call 63/67 (drbg_k still holds the planted
  pattern; hmac_val/hmac_key zero; drbg_buf_idx $00);
  M5 no memory cap 59/66; M6 no stack zero 66/67; M7 no zp_save span 66/67;
  M8 no mul_dma span 65/67.
pytest 416 passed, 3 failed: test_device_prep x2 (_prg_load.py, also on
master) and test_ip65_hw_checks_unit (needs an ip65 build/).
test_drbg_state_owner: PASSED.

Margins vs f0c878f: LOADER -152 B on all eight profiles (ip65 381,
ip65-onchip 381, uci 610, uci-onchip 610, uci-comb 217, ip65-wiki 381,
ip65-onchip-wiki 381, uci-comb-demo-store 141); nothing else moves.
uci-m3 -47 B (32,747 -> 32,700). PRG sha256:
  ip65              5d16e8ac03d186f34813a9eabe85c51b93cf0ceb2fb7bd93a548bffb8e4fd35e
  ip65-onchip       6fe69e793ba64a691f045c67f86e98b35f5996e237ddf500c7bf5c59303af6f2
  uci               85b7d9eaba23b0b03bb19b44994555d3a328f93c10053f3fb6fff88743b140db
  uci-onchip        7964101bd3b341e0b3819a53de1e1bb599d63560a49524b27a17c5ad497a9555
  uci-comb          6dd9b352fa5b86841864d2fbb74cb5ce22fdb12e54df8633d9d90829ae429c68
  ip65-wiki         1e35537c465710e7a050ebe72a6ce1fc9811edcd0ba7dcebb075b13cd4f9b4be
  ip65-onchip-wiki  3726dd1d98cc9082dba827a7ed7eb168d932b52dfbe7908f363ce1b90da598a2
  demo-store        d333577472c754b9edb9310ed25112d83f522dccaa0148845103d815367975cd
  uci-m3            6b3e52af14807a77a6e90a6ba56259c342d83a21e787f3f04e547f1dcc2e2e9a

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

adv-quit's re-attack (BLOCK) on e851c38/9f27f49's cap: with
MEMSIZ = FRETOP = $0803 every quoted string in direct mode is ?OUT OF
MEMORY -- LOAD"P",8, PRINT "HELLO", OPEN with a name -- so nothing could
be loaded after 'Q' without a reset. And it did not hold: OPEN 2,2
(RS-232) copies KERNAL MEMTOP to MEMSIZ via $E0F9, after which DIM
overwrote 2,819 B of the image. The cap is removed; NEW stays.

After 'Q' BASIC owns $0801-$9FFF, which is the image. SYS re-entry into
it is valid only before BASIC allocates anything (variables, strings,
LOAD), and a re-entry that runs crypto clobbers BASIC's ZP again exactly as
before 'Q'. The tools/uci rigs only type SYS after 'Q', so they are fine.
boot.s and CLAUDE.md say exactly that.

Test (test_quit_basic_exit.py):
- OOM/cap checks dropped; MEMSIZ = $A000 checked instead.
- [5] SYS-only re-entry: a typed SYS to a page-3 stub runs and leaves
  $0803-$9FFF byte-identical; X25519 + P-256 re-entry still correct.
- [6] on a second, fresh boot (step 5's crypto re-clobbers BASIC's ZP):
  'Q', then PRINT "AB"+"CD" -> ABCD, and LOAD"P",8 of "10 A=1:PRINT A"
  from a d64 minted with c1541 loads, and RUN prints a line "1".
- Every post-'Q' step is tallied, never raised: a jammed 6510 (M3) now
  fails the remaining checks by name.
- drbg_k wording: the check proves K was replaced by a one-way update at
  quit, not specifically by instantiate.

Red (master uci-onchip 45bea88a, VICE): 10/22 passed, exit 1 (incl.
PRINT "AB"+"CD", and LOAD -> ?OUT OF MEMORY: master leaves VARTAB past
MEMSIZ).
Green (VICE): uci-onchip 192157cb... 68/68; ip65-onchip 78a10748... 68/68.
Mutants (uci-onchip), all red, all by tally: M1 no poly_prod span 67/68;
M2 no INITCZ 59/68; M3 no NEW 66/68; M4 no drbg_init_entropy tail call
64/68; M6 no stack zero 67/68; M7 no zp_save span 67/68; M8 no mul_dma
span 66/68. M5/M10/M11 (the cap) retired.
test_drbg_state_owner and test_runner_coverage PASS; pytest 417 passed,
3 failed (test_device_prep x2, also on master; test_ip65_hw_checks_unit
needs an ip65 build/).

Hardware: U64E 10.43.23.83, fw 3.15 3a1ff9ff, FPGA 126, core 1.50, 48 MHz,
dual lock. uci-onchip, HTTPS_HOST=en.wikipedia.org HTTPS_PATH=/robots.txt,
PRG 4354278dd05d7269ba1e2360cecf69434bce7b5b4f5a2d92841dbb6fcdbb97b7:
rig_https_banner PASS (HTTP 200, 28,275 B == Content-Length). After 'Q':
READY. with no error, CHRGET == ROM, MEMSIZ=$A000; PRINT 6*7 -> 42;
PRINT "AB"+"CD" -> ABCD; the same d64 mounted read-only on drive a
(empty before, removed after): LOAD"P",8 -> SEARCHING/LOADING/READY.,
RUN -> 1.

Margins vs f49d8ab: LOADER -140 B on all eight profiles (ip65 393,
ip65-onchip 393, uci 622, uci-onchip 622, uci-comb 229, ip65-wiki 393,
ip65-onchip-wiki 393, uci-comb-demo-store 153); nothing else moves.
uci-m3 MAIN -35 B (32,712 free). PRG sha256:
  ip65              0a0b7a78660c298b510edac564a4c6757f89aaafd46369a7faf5f1c107a53c17
  ip65-onchip       78a10748f42553417f5e8a5bfafab9cbc9e70a10fa2821d99dd2680c09753546
  uci               3edcb6a61845cf53844136639781d721b37bcd463b15887720936eadd38b348a
  uci-onchip        192157cb6e326a4badfd49492f6ef83df1a9be2bdd1f7d5da3fa77f02cdb4c7e
  uci-comb          30cca7820ac528a7ab372dd78e5049ebd0ab65dbfc15879c4db08edb55c2475c
  ip65-wiki         7ca7eba5d946b2908b29d42014055e380572b6cd063f273bf725d63519206198
  ip65-onchip-wiki  a24d051552d5e395700c7dd0dd18eae43d28d02a9a5dd7555fe3e9eb18459638
  demo-store        d0c32ffcdf1347c4adc0fd0eb79f879d6c85cd90110c5a1fb544a41f6ff4e171
  uci-m3            d2f0201c4e781194ee71679c2bf26abb7348682f2d26471d1e57622b999f6ba3

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- boot.s / CLAUDE.md: after a crypto SYS re-entry returns BASIC is
  unusable; rigs park the CPU (JMP *) or type only further SYS lines.
- boot.s: the "capping MEMSIZ was tried" story moves to
  docs/engineering-notes.md (Known issues); boot.s keeps a one-line
  "do not cap MEMSIZ" guard.
- test_quit_basic_exit.py docstring: a missing c1541 is two counted
  FAILs by design (skip policy).

PRG sha256 unchanged:
  uci-onchip   192157cb6e326a4badfd49492f6ef83df1a9be2bdd1f7d5da3fa77f02cdb4c7e
  ip65-onchip  78a10748f42553417f5e8a5bfafab9cbc9e70a10fa2821d99dd2680c09753546

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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