From 307df67f9da9b5b886d7a5f1964f39dc19d1f090 Mon Sep 17 00:00:00 2001 From: Arti Zirk Date: Sat, 17 Oct 2026 14:14:07 +0300 Subject: [PATCH] harness/mame: the TFTP PCF loader skips BITMAPS, so the n*4 overflow is not on that path The TFTP font path loads through 3F96B850 and parses with the metadata reader 3F97EF00, which handles only PROPERTIES/ACCELERATORS/METRICS/ ENCODINGS and silently skips BITMAPS. A font with num_chars=0x40000001 therefore passes the loader (LOADER-A-SUCCESS) without the field ever being read, and pcfReadBitmaps (3F984970, multiply 3F985050) is never entered. pcfReadBitmaps has exactly one caller, 3F98BB70, in the X11 font-request reader 3F98AC20, not the TFTP fetch path. The 'refused after 1024 bytes' of evil-real.pcf is the METRICS table: its big-endian compressed count 0x00DF is read little-endian as 57088, so fosAlloc(57088*16) fails at ~906 bytes; BITMAPS (offset 2024) is never reached. Baseline.pcf fails the same way; baseline_le.pcf puts that one 2-byte field in the order the loader reads it, parses cleanly, and still ignores the crafted BITMAPS count. No ROM patch, no check disabled, no serve.py edit. Baselines unchanged: no env samples=198, stub samples=53. --- harness/mame/PCF_VALIDATION_REPORT.md | 274 ++++++++++++++++++++++++ harness/mame/pcfwork/make_candidates.py | 56 +++++ harness/mame/pcfwork/pcfflow.lua | 49 +++++ harness/mame/pcfwork/pcftrace.lua | 91 ++++++++ harness/mame/pcfwork/run_candidate.sh | 45 ++++ harness/mame/pcfwork/run_index.sh | 44 ++++ harness/mame/pcfwork/serve_candidate.py | 52 +++++ harness/mame/pcfwork/serve_index.py | 54 +++++ 8 files changed, 665 insertions(+) create mode 100644 harness/mame/PCF_VALIDATION_REPORT.md create mode 100644 harness/mame/pcfwork/make_candidates.py create mode 100644 harness/mame/pcfwork/pcfflow.lua create mode 100644 harness/mame/pcfwork/pcftrace.lua create mode 100755 harness/mame/pcfwork/run_candidate.sh create mode 100755 harness/mame/pcfwork/run_index.sh create mode 100644 harness/mame/pcfwork/serve_candidate.py create mode 100644 harness/mame/pcfwork/serve_index.py diff --git a/harness/mame/PCF_VALIDATION_REPORT.md b/harness/mame/PCF_VALIDATION_REPORT.md new file mode 100644 index 0000000..deb8c7f --- /dev/null +++ b/harness/mame/PCF_VALIDATION_REPORT.md @@ -0,0 +1,274 @@ +# What the PCF loader validates on the TFTP font path — and why it cannot reach `pcfReadBitmaps` + +Date: 2026-10-17. Firmware `vt1200_v22.bin`, md5 +`b46a3e442ccb86134d2356bd70d22fd2` (**unmodified**). No change to +`harness/mame/vt1200.cpp`, `build.sh`, `harness/serve.py`, or the MAME tree. No +ROM patch and no check was disabled. Scratch and instrumentation live in +`harness/mame/pcfwork/`. + +## Verdict (the short answer) + +**The TFTP font path does not use `pcfReadBitmaps` at all.** + +It loads the file through `3F96B850`, which recognises the PCF magic and calls a +**metadata reader** (`3F97EF00`, "parser A"). Parser A handles four table types — +PROPERTIES, ACCELERATORS, METRICS, ENCODINGS — and **silently skips the BITMAPS +table**. The crafted `num_chars = 0x40000001` is therefore never read on this +path, let alone multiplied. A font can pass the loader's validation (and it +does), but passing it does not reach the vulnerable arithmetic. + +The "refused after 1024 bytes" observation was **not** a `pcfReadBitmaps` +validation. For `evil-real.pcf` the parser gives up on the **METRICS** table: +the compressed-metrics count is read little-endian as 57,088 instead of the +intended 223, so `fosAlloc(57088*16 = 913,408)` fails. It never reaches BITMAPS +(offset 2024); it consumed about 906 bytes, which is why the transfer looked like +it stopped at the 1024-byte window. + +`pcfReadBitmaps` still exists and is still called — but only by a **different** +parser (`3F98AC20`, "parser B") on the **X11 font-request** path, not by the +TFTP file loader. That path was not demonstrated from a TFTP-delivered font (see +"Remaining open question" below). + +## Address convention (confirmed) + +The TMS34010 is bit-addressed and the ROM is mapped at bit `0x3F200000`. +Therefore **bit address = `0x3F200000` + (ROM file offset × 8)**. The earlier +notes' `0x0F09CE` is byte `0x0F09CE` = bit `0x3F984E70`, which is exactly +`pcfReadBitmaps`' arithmetic block. All addresses below are bit addresses. + +## 1. The path a TFTP-delivered font actually takes + +Live trace of a valid fetched font (trace script: `pcfwork/pcftrace.lua`): + +``` +FONTLOAD 3F96B850 +FONT-MAGIC magic=70636601 +PCF->parserA 3F96BB80 -> L3F97EF00 +PARSER-A-ENTRY 3F97EF00 +``` + +`3F96B850` reads the first four bytes, stores them in the font object +(`*a9(0x400)`), and compares them with `0x70636601` +(`3F96BB20: cmpi 0x70636601,a5`). PCF goes to `L3F97EF00`; anything else falls to +the DEC-SNF check (`0x1DF5E76`). The name comparison between the "path file" +name and the font's own name is at `3F96C6B0`, and the success return +(`movk 1,a8`) is at `3F96C930`. + +The same loader/parser pair is the only one exercised by the fetch: in a +150-emulated-second run with a font parser A accepts, parser B and parser C were +**never** entered. + +## 2. What parser A validates + +Parser A (`3F97EF00`) reads `nTables` and up to **eight** TOC entries +(`label, format, size, start`, 16 bytes each). It then walks those entries in +**TOC order**, seeks to each `start`, and dispatches on the label +(`L3F9811B0`): + +| label | table | handler | reader | what is checked | +|---|---|---|---|---| +| 1 | PROPERTIES | `L3F97F8A0` | `L3F9770B0` | `format & 0xFFFFFF00 == 0`; `nProps`; two allocations; reads `name/type/value` then the string table; "Unkown property format", "allocation failure … (props/isstring/tmpbuf)", "short read … (tmpbuf)" | +| 2 | ACCELERATORS | `L3F980200` | `L3F979C00` | `format & 0xFFFFFF00 == 0`; fixed 100-byte body; "Unknown accelerator format" | +| 4 | METRICS | `L3F980E40` | `L3F9886B0` | format must be default `0` or compressed `0x100`; **count cross-checked against `pCS->nChars`** ("bad char count"); allocation `nChars*12` / `nChars*16` | +| 32 | ENCODINGS | `L3F980A20` | `L3F97C750` | `format & 0xFFFFFF00 == 0`; first/last col/row; each index `< nChars` ("Illegal char index") | +| 8,16,64,128 | BITMAPS, INK_METRICS, SWIDTHS, GLYPH_NAMES | — | — | **skipped** (`L3F9812B0`: `inc a9`, next entry) | + +The dispatch is unambiguous: + +``` +3f9811e0: move *a7(0x80),a8,1 ; a8 = label +3f981200: dec a8 +3f981210: jaeq L3F97F8A0 ; 1 -> properties +3f981230: dec a8 +3f981240: jaeq L3F980200 ; 2 -> accelerators +3f981260: subk 2,a8 +3f981270: jreq L3F980E40 ; 4 -> metrics +3f981280: subk 28,a8 +3f981290: jaeq L3F980A20 ; 32 -> encodings +3f9812b0: inc a9 ... ; everything else -> next TOC entry +``` + +The seek before each handler (`3F96F9F0`) returns ≤ 0 on failure, which reaches +the `Failed to read PCF format ` messages (`3F97FA40`, `3F9803A0`, +`3F981050`, `3F980BB0`). + +**What is not there:** there is no reader for label 8. Parser A never reads the +BITMAPS count, offsets or glyph data. Consequently there is no `num_chars` +validation on this path either — the field is simply not looked at. + +## 3. Where `pcfReadBitmaps` actually is, and who calls it + +`pcfReadBitmaps` is `3F984970`; its arithmetic block is `3F984E70`: + +``` +3f984e70: move *a9(0x270),a11,1 ; nChars +3f984e90: jrne L3F984EE0 +3f984ea0: move *a13(0x20),*a9(0x270),1 ; nChars == 0 -> nChars = n +3f984f20: cmp a7,a11 ; else compare n with nChars +3f984f30: jreq L3F985020 +3f984f80: "bad count in pcfReadBitmaps (expected %d, got %d)" +3f985030: move *a13(0x20),a11,1 ; n +3f985050: sll 0x2,a11 ; n*4 <-- unchecked +3f985090: calla L3F703D90 ; fosAlloc(n*4,1) +3f985260: (loop) read n CARD32 offsets at pBitOffsets + i*4 +``` + +The ROM string `allocation failure in pcfReadBitmaps (offsets,%d)` is the +`fosAlloc` failure report at `3F985150` — it names the **size argument**, it is +not a separate validation of the offsets table. There is no check that `n` +matches the table size, nor that `n*4` did not wrap. + +There is **exactly one caller** in the ROM: `3F98BB70`, inside parser B +(`3F98AC20`). Parser B is not the TFTP loader. It is reached from the X11 +request dispatcher: +``` +3F960640 (inside the X11 request handler that prints + "Invalid request code %d received.", string 3F98EA50 at 3F960750) + -> 3F9643E0 (font resource lookup: id -> font, then the file cache) + -> 3F964850 (PCF/SNF dispatch over the cached font object, magic at +0x400) + -> 3F98AC20 (parser B) + -> 3F98BB70 -> pcfReadBitmaps +``` + +Parser B's own TOC dispatch (`3F98C320`) handles labels 2, 4, 8 and 32, and +calls `pcfReadBitmaps` for label 8. It *also* has the count cross-check +(`3F984F20`/`3F984F80`), so on parser B a mismatched `num_chars` is caught by +default. + +So `pcfReadBitmaps` belongs to the X11 font-request reader (font-id lookup over +the cached font data); it is not the code that parses the file while it is being +fetched. + +## 4. Live evidence + +All runs: `VT1200_SKIP_LEVEL1=1`, the private cfg selecting `taptun`, a private +`-nvram_directory` seeded from a configured NVRAM (TFTP font path +`192.168.99.1:69`), and the TFTP server serving the candidate. Trace output is +`pcfwork/pcftrace.lua`. + +| candidate | result | +|---|---| +| `reference/fonts/baseline.pcf` (680 B) | Parser A: TOC 1,2,4 → **`A-FAIL metrics`**. The compressed-metrics count `00 01` is read little-endian as `0x0100` = 256, not 1; the metrics reader then returns 0. | +| `pcfwork/baseline_le.pcf` (680 B; metrics count byte-swapped to `01 00`) | Parser A: TOC 1,2,4 (metrics count 1, `A-METR-C done`, `readerret=1`), then 8,16,32,64,128. No failure. | +| `pcfwork/baseline_le_evil.pcf` (same, **BITMAPS `num_chars=0x40000001`**) | **Identical trace** to the line above: all eight TOC entries, `LOADER-A-SUCCESS` at frame 395. `BITMAPS-ENTRY`, `BITMAPS-ARITH` and `MULTIPLY` never hit; parser B and parser C never entered. The crafted count is ignored, not validated. | +| `reference/fonts/evil-real.pcf` (25,860 B) | Parser A: TOC 1 (properties, 648 B), 2 (accelerators), 4 (metrics at 900). `fmt=0x10E` (compressed), count bytes `00 DF` read little-endian = `0xDF00` = **57,088**; `fosAlloc(57088*16 = 913,408)` returns 0 → **`A-FAIL metrics`**. BITMAPS is at offset 2024 and is never reached. ≈906 bytes consumed. | + +The last row is the "1024-byte" stop: the parser bailed at the METRICS +allocation after ~906 bytes, so only the first two 512-byte TFTP blocks were +ever consumed. It is not a validation of `num_chars`, and it is not a size limit +of the transfer path. + +### Why the byte order matters (and why it is not cheating) + +`bdftopcf` on this host writes the compressed-metrics count big-endian +(`00 01`); parser A copies the two bytes with a plain `fosRead` and reads them +little-endian (`3F9898B0`, `3F9899F0: setf 16,1,0; move *a13,a7,0`). The stock +font is therefore misread. `baseline_le.pcf` is the **same font** with that one +2-byte field in the order the loader actually reads it. Nothing is disabled; the +loader's own checks still run. The generator is +`pcfwork/make_candidates.py`. + +## 5. Judgement + +**Closed, for the TFTP font path.** A font can pass the loader's validation +(`LOADER-A-SUCCESS`), but that validation is not the one guarding the multiply: +the loader never reads the BITMAPS table at all, so `num_chars = 0x40000001` +cannot reach `pcfReadBitmaps`. The "1024-byte refusal" is a METRICS failure +caused by the loader's little-endian read of a big-endian field, not a +`pcfReadBitmaps` count/offset check. + +This is not "the validation happens to be good enough"; it is stronger: **the +vulnerable function is not on this route**. + +### Remaining open question (stated, not papered over) + +`pcfReadBitmaps` is still present and still called by parser B on the X11 +font-request path (`3F960640 → 3F9643E0 → 3F964850 → 3F98AC20`). Parser B reads +the **cached** font bytes, so in principle a font that parser A accepted and +that the terminal then *opened* would be parsed by parser B. Two things were not +demonstrated: + +1. that a TFTP-registered font is ever opened (the harness has no X11 client, and + in 150 emulated seconds the terminal fetched the index but never requested the + served font file); and +2. whether reordering the TOC so BITMAPS precedes METRICS would leave + `pCS->nChars == 0` when `pcfReadBitmaps` runs, bypassing parser B's cross-check. + (Parser A would not object: it skips BITMAPS and reads METRICS whenever it + occurs.) + +That is a separate trigger (an X11/font-server `OpenFont` of a crafted font), not +the "serve the font over TFTP and let the loader parse it" route this work was +about. It is recorded here so the next session starts from the right place +rather than from the earlier assumption that the TFTP loader reaches +`pcfReadBitmaps`. + +## 6. What I refused to fake + +* No ROM patch; `md5sum vt1200_v22.bin` = `b46a3e442ccb86134d2356bd70d22fd2` + before and after. +* No check disabled, no `harness/serve.py` edit, no invented loader behaviour. +* I did **not** use `overflow.lua` (jumping straight into `3f984e70`) as evidence + of reachability. The reachability statement above comes from the real path: + TFTP fetch → `3F96B850` → parser A. +* The one constructed input (`baseline_le.pcf`) is the project's valid font with + a 2-byte field put in the byte order the loader actually reads; the loader's + checks still run and still pass on their own terms. +* `evil-real.pcf` is reported as a METRICS failure even though that makes a + "nicer" BITMAPS story impossible. + +## 7. Reproduction + +```sh +cd /home/arti/code/k-space/vt1200 +python3 harness/mame/pcfwork/make_candidates.py + +# isolated tap (does not touch the owner's tap-mess-1000-0) +sudo ip netns add vt1test +sudo ip netns exec vt1test ip link set lo up +sudo ip netns exec vt1test ip tuntap add dev tap-mess-0-0 mode tap +sudo ip netns exec vt1test ip addr add 192.168.99.1/24 dev tap-mess-0-0 +sudo ip netns exec vt1test ip link set tap-mess-0-0 up + +# a configured NVRAM copy (TFTP font path 192.168.99.1:69) +mkdir -p harness/mame/pcfwork/nvram-run/vt1200 +cp harness/mame/pcfwork/nvram-pristine.bin harness/mame/pcfwork/nvram-run/vt1200/nvram + +harness/mame/pcfwork/run_candidate.sh \ + harness/mame/pcfwork/baseline_le_evil.pcf 40 +``` + +`run_candidate.sh` starts `serve_candidate.py` (baseline for the path/index +names, the candidate for the derived font name), runs MAME with +`-nvram_directory pcfwork/nvram-run`, and prints the parser trace from +`debug.log`. Switch the candidate to `reference/fonts/evil-real.pcf` for the +25 KB case. The NVRAM copy is not committed: it is the owner's settings block +(font path, IP) copied for the run, not part of the harness. + +## 8. Regression baselines (unchanged) + +No source file used by the driver changed, so every configuration is intact; the +two documented change detectors were re-run on the shipped binary: + +``` +no env, progress.lua 200 frames : PC=3FDCE400 samples=198 (3 distinct PCs) +VT1200_IO_PATH_STUB=1, 200 fr : PC=3FDCE400 samples=53 (75 distinct PCs) +``` + +`VT1200_SKIP_LEVEL1=1` and `VT1200_MOUSE=1` were used by these runs on the same +unmodified driver; no code path they depend on was touched. + +## 9. New files + +``` +harness/mame/pcfwork/ + pcftrace.lua parser A / parser B / pcfReadBitmaps / TFTP breakpoints + pcfflow.lua coarse call-graph trace of the font path + make_candidates.py baseline_le.pcf and baseline_le_evil.pcf + serve_candidate.py path/index -> baseline, font name -> candidate + serve_index.py a real fonts.dir + candidate (index experiments) + run_candidate.sh one headless real-path run, in the netns + run_index.sh same, with serve_index.py +``` + +No existing file was edited for this work except this report and the new +`pcfwork/` files. diff --git a/harness/mame/pcfwork/make_candidates.py b/harness/mame/pcfwork/make_candidates.py new file mode 100644 index 0000000..dff2cee --- /dev/null +++ b/harness/mame/pcfwork/make_candidates.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +"""Build the two PCF candidates used in PCF_VALIDATION_REPORT.md. + +`baseline_le.pcf` is the project's valid 680-byte font with ONE change: the +compressed-metrics count in the METRICS table is byte-swapped from `00 01` +(big-endian 1, what bdftopcf emits) to `01 00`. The VT1200's font loader reads +that INT16 raw (little-endian), so the stock file is read as 0x0100 = 256 +characters and the metrics parse runs off the end of the table. The swap makes +the loader read the intended count of 1. It is a construction of a font the +loader accepts, not a disabled check. + +`baseline_le_evil.pcf` is that font with the BITMAPS `num_chars` set to +0x40000001 (the value overflow.lua used) -- to show the loaded path ignores it. + + python3 harness/mame/pcfwork/make_candidates.py +""" +import os +import struct +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE))) +sys.path.insert(0, os.path.join(ROOT, "tools")) +import fontgen # noqa: E402 + +SRC = os.path.join(ROOT, "reference", "fonts", "baseline.pcf") + + +def main(): + with open(SRC, "rb") as fh: + data = bytearray(fh.read()) + + metrics = fontgen.find_table(bytes(data), fontgen.METRICS) + if metrics is None: + raise SystemExit("baseline.pcf has no METRICS table") + off = metrics["offset"] + fmt = struct.unpack_from("parserC') +bp('3f962cb0', 'OPEN-else-SNF') +bp('3f9643e0', 'FN-B-ENTRY') +bp('3f964850', 'FN-B-850') +bp('3f964900', 'FN-B->parserB') +bp('3f96d300', 'LOAD-A-call') +bp('3f96b850', 'LOADER-A-ENTRY') +bp('3f96bb80', 'LOADER-A->parserA') +bp('3f96c990', 'CACHE-LOOKUP') +-- Parsers. +bp('3f97ef00', 'PARSER-A-ENTRY') +bp('3f981690', 'PARSER-C-ENTRY') +bp('3f98ac20', 'PARSER-B-ENTRY') +bp('3f984970', 'pcfReadBitmaps-ENTRY') +bp('3f985060', 'pcfReadBitmaps-MULTIPLY') +-- Other font-file reader with SNF dispatch (3f964850's sibling at 3f96b850 is loader A). +bp('3f964ac0', 'FN-B-SNF-check') + +-- pcfReadFont outer dispatch of parser B (label). +bp('3f98c370', 'B-TOC') +-- pcfReadFont-ish wrappers: string messages tell us which reader bails. +bp('3f98ef08', 'ERR-msg-3f98ef08') +bp('3f98f088', 'ERR-msg-3f98f088') +bp('3f98f210', 'ERR-msg-3f98f210') +bp('3f98f428', 'ADDR-3f98f428') + +local maxframes = tonumber(os.getenv("PCT_FRAMES") or "20000") +emu.register_frame_done(function() + FRAME = FRAME + 1 + if FRAME % 4000 == 0 then + print(string.format("frame %d PC=%08X", FRAME, cpu.state['PC'].value)) + end + if FRAME >= maxframes then print("done"); machine:exit() end +end, "vt1200-flow") +dbg.visible_cpu = cpu +dbg.execution_state = 'run' diff --git a/harness/mame/pcfwork/pcftrace.lua b/harness/mame/pcfwork/pcftrace.lua new file mode 100644 index 0000000..9f54c47 --- /dev/null +++ b/harness/mame/pcfwork/pcftrace.lua @@ -0,0 +1,91 @@ +-- pcftrace.lua -- live trace of the firmware's PCF font parser(s). +-- +-- There are TWO PCF readers in the ROM: +-- * parser A L3F97EF00 (reached from the font-file loader at 3f96bb80) +-- * parser B L3F98AC20 (reached from 3f964900) -- the only caller of the +-- pcfReadBitmaps function (L3F984970) that holds the n*4 overflow. +-- This traces both, plus the font-file dispatch, so the run says which reader +-- the TFTP font path actually uses and where it bails. + +local machine = manager.machine +local dbg = machine.debugger +local cpu = machine.devices[':maincpu'] + +if not dbg then + print("ERROR: debugger not enabled; pass -debug") + machine:exit() + return +end + +FRAME = 0 + +-- Font-file loader / dispatch. +dbg:command('bpset 3f96b850,,{ printf "FONTLOAD fr=%d\\n", FRAME ; g }') +dbg:command('bpset 3f96bb20,,{ printf "FONT-MAGIC fr=%d magic=%08X\\n", FRAME, a5 ; g }') +dbg:command('bpset 3f96bb80,,{ printf "PCF->parserA fr=%d\\n", FRAME ; g }') +dbg:command('bpset 3f96c930,,{ printf "LOADER-A-SUCCESS fr=%d\\n", FRAME ; g }') +dbg:command('bpset 3f96d300,,{ printf "LOAD-A-call fr=%d\\n", FRAME ; g }') + +-- Parser A entry. +dbg:command('bpset 3f97ef00,,{ printf "PARSER-A-ENTRY fr=%d\\n", FRAME ; g }') +-- Parser A validations (labels 1/2/4/32). +dbg:command('bpset 3f97fa40,,{ printf "A-FAIL properties fr=%d\\n", FRAME ; g }') +dbg:command('bpset 3f9803a0,,{ printf "A-FAIL accelerators fr=%d\\n", FRAME ; g }') +dbg:command('bpset 3f980bb0,,{ printf "A-FAIL encodings fr=%d\\n", FRAME ; g }') +dbg:command('bpset 3f981050,,{ printf "A-FAIL metrics fr=%d\\n", FRAME ; g }') +-- Parser A dispatch (label in a8 right after it is loaded). +dbg:command('bpset 3f981200,,{ printf "A-TOC fr=%d i=%d label=%d\\n", FRAME, a9, a8 ; g }') + +-- Parser A metrics handler internals. +dbg:command('bpset 3f980e40,,{ printf "A-METRICS fr=%d\\n", FRAME ; g }') +dbg:command('bpset 3f980f20,,{ printf "A-METRICS seekret=%08X\\n", a8 ; g }') +dbg:command('bpset 3f980ff0,,{ printf "A-METRICS callreader fr=%d\\n", FRAME ; g }') +dbg:command('bpset 3f981020,,{ printf "A-METRICS readerret=%08X\\n", a8 ; g }') +dbg:command('bpset 3f9886b0,,{ printf "A-METR-ENTRY fr=%d\\n", FRAME ; g }') +dbg:command('bpset 3f9889a0,,{ printf "A-METR fmtread ret=%08X fmt=%08X\\n", a8, d@(a13+0x20) ; g }') +dbg:command('bpset 3f988a40,,{ printf "A-METR compressed fr=%d\\n", FRAME ; g }') +dbg:command('bpset 3f988b90,,{ printf "A-METR badfmt fr=%d\\n", FRAME ; g }') +dbg:command('bpset 3f989820,,{ printf "A-METR-C-ENTRY fr=%d\\n", FRAME ; g }') +dbg:command('bpset 3f989940,,{ printf "A-METR-C countread ret=%08X count=%04X\\n", a8, d@(a13) ; g }') +dbg:command('bpset 3f989ac0,,{ printf "A-METR-C badcount fr=%d nChars=%08X count=%04X\\n", FRAME, d@(a13+0xffc0)+0, d@(a13) ; g }') +dbg:command('bpset 3f989c80,,{ printf "A-METR-C allocret=%08X\\n", a8 ; g }') +dbg:command('bpset 3f989f20,,{ printf "A-METR-C byteread-fail fr=%d\\n", FRAME ; g }') +dbg:command('bpset 3f98a120,,{ printf "A-METR-C byteread-fail2 fr=%d\\n", FRAME ; g }') +dbg:command('bpset 3f98a970,,{ printf "A-METR-C done fr=%d\\n", FRAME ; g }') +dbg:command('bpset 3f988f80,,{ printf "A-METR done fr=%d\\n", FRAME ; g }') + +-- Parser B entry and its pcfReadBitmaps call. +dbg:command('bpset 3f98ac20,,{ printf "PARSER-B-ENTRY fr=%d\\n", FRAME ; g }') +dbg:command('bpset 3f98bb70,,{ printf "B-CALL-BITMAPS fr=%d\\n", FRAME ; g }') + +-- pcfReadBitmaps internals. +dbg:command('bpset 3f984970,,{ printf "BITMAPS-ENTRY fr=%d\\n", FRAME ; g }') +dbg:command('bpset 3f984e70,,{ printf "BITMAPS-ARITH fr=%d nChars=%08X n=%08X\\n", FRAME, d@(a9+0x270), d@(a13+0x20) ; g }') +dbg:command('bpset 3f985060,,{ printf "MULTIPLY fr=%d n4=%08X n=%08X\\n", FRAME, a11, d@(a13+0x20) ; g }') +dbg:command('bpset 3f9850e0,,{ printf "OFFSETS-ALLOC fr=%d result=%08X\\n", FRAME, a8 ; g }') +dbg:command('bpset 3f984f80,,{ printf "VALIDATE bad count fr=%d nChars=%08X n=%08X\\n", FRAME, d@(a9+0x270), d@(a13+0x20) ; g }') +dbg:command('bpset 3f985150,,{ printf "VALIDATE alloc-offsets-fail fr=%d\\n", FRAME ; g }') +dbg:command('bpset 3f9856b0,,{ printf "VALIDATE alloc-glyphs-fail fr=%d\\n", FRAME ; g }') +dbg:command('bpset 3f985860,,{ printf "VALIDATE short-read-glyphs fr=%d\\n", FRAME ; g }') +dbg:command('bpset 3f98b8c0,,{ printf "B-FAIL PCF metrics fr=%d\\n", FRAME ; g }') +dbg:command('bpset 3f98bbe0,,{ printf "B-FAIL PCF bitmaps fr=%d\\n", FRAME ; g }') +dbg:command('bpset 3f98bed0,,{ printf "B-FAIL PCF accelerators fr=%d\\n", FRAME ; g }') +dbg:command('bpset 3f98c1b0,,{ printf "B-FAIL PCF encodings fr=%d\\n", FRAME ; g }') + +-- The TFTP font fetch, for the record. +dbg:command('bpset 3f5ed230,,{ printf "TFTP-READ fr=%d\\n", FRAME ; g }') + +local maxframes = tonumber(os.getenv("PCT_FRAMES") or "20000") +emu.register_frame_done(function() + FRAME = FRAME + 1 + if FRAME % 2000 == 0 then + print(string.format("frame %d PC=%08X", FRAME, cpu.state['PC'].value)) + end + if FRAME >= maxframes then + print("done (" .. FRAME .. " frames)") + machine:exit() + end +end, "vt1200-pct") + +dbg.visible_cpu = cpu +dbg.execution_state = 'run' diff --git a/harness/mame/pcfwork/run_candidate.sh b/harness/mame/pcfwork/run_candidate.sh new file mode 100755 index 0000000..186e2b7 --- /dev/null +++ b/harness/mame/pcfwork/run_candidate.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# Run one candidate font through the terminal's own TFTP loader, with the PCF +# parser traced. Everything happens inside the isolated netns vt1test so the +# owner's tap is untouched. +# +# run_candidate.sh [seconds] [trace.lua] [extra mame args...] +set -euo pipefail +NS=vt1test +ROOT=/home/arti/code/k-space/vt1200 +WORK=$ROOT/harness/mame/pcfwork +CAND="${1:?usage: run_candidate.sh [seconds] [trace.lua]}" +SECS="${2:-40}" +TRACE="${3:-$WORK/pcftrace.lua}" +shift 3 2>/dev/null || true + +sudo -n ip netns exec "$NS" pkill -x vt1200 2>/dev/null || true +sudo -n ip netns exec "$NS" pkill -f serve_candidate.py 2>/dev/null || true +sleep 0.3 +rm -rf "$WORK/nvram-run" "$WORK/debug.log" +mkdir -p "$WORK/nvram-run/vt1200" +cp "$WORK/nvram-pristine.bin" "$WORK/nvram-run/vt1200/nvram" + +sudo -n ip netns exec "$NS" python3 -u "$WORK/serve_candidate.py" "$CAND" \ + >"$WORK/serve.log" 2>&1 & +SRV=$! +sleep 1.2 + +cd "$WORK" +timeout $((SECS+15)) sudo -n ip netns exec "$NS" env -u DISPLAY \ + SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \ + XDG_RUNTIME_DIR="$WORK" VT1200_SKIP_LEVEL1=1 \ + /home/arti/code/k-space/mame0289/vt1200 vt1200 \ + -rompath "$ROOT/harness/mame/roms" -video none -sound none -nothrottle \ + -skip_gameinfo -cfg_directory "$WORK/cfg" \ + -nvram_directory "$WORK/nvram-run" -networkprovider taptun \ + -debug -debugger none -debuglog \ + -autoboot_script "$TRACE" -autoboot_delay 0 \ + -seconds_to_run "$SECS" >"$WORK/mame_run.log" 2>&1 || true + +kill $SRV 2>/dev/null || true +sudo -n ip netns exec "$NS" pkill -f serve_candidate.py 2>/dev/null || true +echo "=== server ===" +cat "$WORK/serve.log" +echo "=== parser trace ===" +grep -E "TOC|BITMAPS|MULTIPLY|OFFSETS|VALIDATE|FAIL|TFTP-READ" "$WORK/debug.log" | head -200 diff --git a/harness/mame/pcfwork/run_index.sh b/harness/mame/pcfwork/run_index.sh new file mode 100755 index 0000000..83646d6 --- /dev/null +++ b/harness/mame/pcfwork/run_index.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# Run one candidate through a proper fonts.dir index. +# run_index.sh [seconds] [trace.lua] [font-file] [dir] +set -euo pipefail +NS=vt1test +ROOT=/home/arti/code/k-space/vt1200 +WORK=$ROOT/harness/mame/pcfwork +CAND="${1:?usage: run_index.sh [seconds] [trace.lua]}" +XLFD="${2:?XLFD name}" +SECS="${3:-150}" +TRACE="${4:-$WORK/pcftrace.lua}" +FONT_FILE="${5:-v1200a.pcf}" +DIR="${6:-69}" + +sudo -n ip netns exec "$NS" pkill -x vt1200 2>/dev/null || true +sudo -n ip netns exec "$NS" pkill -f serve_index.py 2>/dev/null || true +sleep 0.3 +rm -rf "$WORK/nvram-run" "$WORK/debug.log" +mkdir -p "$WORK/nvram-run/vt1200" +cp "$WORK/nvram-pristine.bin" "$WORK/nvram-run/vt1200/nvram" + +sudo -n ip netns exec "$NS" python3 -u "$WORK/serve_index.py" \ + "$CAND" "$XLFD" "$FONT_FILE" "$DIR" >"$WORK/serve.log" 2>&1 & +SRV=$! +sleep 1.2 + +cd "$WORK" +timeout $((SECS+15)) sudo -n ip netns exec "$NS" env -u DISPLAY \ + SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \ + XDG_RUNTIME_DIR="$WORK" VT1200_SKIP_LEVEL1=1 \ + /home/arti/code/k-space/mame0289/vt1200 vt1200 \ + -rompath "$ROOT/harness/mame/roms" -video none -sound none -nothrottle \ + -skip_gameinfo -cfg_directory "$WORK/cfg" \ + -nvram_directory "$WORK/nvram-run" -networkprovider taptun \ + -debug -debugger none -debuglog \ + -autoboot_script "$TRACE" -autoboot_delay 0 \ + -seconds_to_run "$SECS" >"$WORK/mame_run.log" 2>&1 || true + +kill $SRV 2>/dev/null || true +sudo -n ip netns exec "$NS" pkill -f serve_index.py 2>/dev/null || true +echo "=== server ===" +cat "$WORK/serve.log" +echo "=== trace ===" +grep -vE "Breakpoint|version|targeting|^$" "$WORK/debug.log" | head -200 diff --git a/harness/mame/pcfwork/serve_candidate.py b/harness/mame/pcfwork/serve_candidate.py new file mode 100644 index 0000000..12e9fcb --- /dev/null +++ b/harness/mame/pcfwork/serve_candidate.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +"""pcfwork: serve one candidate font over TFTP on the real path. + +The terminal's TFTP font path in the (copied) NVR is 192.168.99.1:69, so it +first RRQs '69' (the path object), then the font name it derives, then +'/fonts.dir'. This serves baseline.pcf for the path/index names (so the +terminal derives a name and asks again) and the candidate font for everything +else -- the same shape the prior font-work used to make the terminal fetch the +crafted font over its own loader. + + python3 serve_candidate.py [bind-ip] +""" +import os +import sys +import time + +ROOT = "/home/arti/code/k-space/vt1200" +sys.path.insert(0, os.path.join(ROOT, "tools")) +import fontgen # noqa: E402 + +cand_path = sys.argv[1] +host = sys.argv[2] if len(sys.argv) > 2 else "192.168.99.1" + +with open(os.path.join(ROOT, "reference/fonts/baseline.pcf"), "rb") as fh: + base = fh.read() +with open(cand_path, "rb") as fh: + cand = fh.read() +print(f"path/index -> baseline.pcf ({len(base)}B); font -> {os.path.basename(cand_path)} ({len(cand)}B)", + flush=True) + + +def resolve(name): + # Path object / index fetches get the baseline so the terminal derives a + # name and re-fetches; everything else gets the candidate. + if name == "69" or name.endswith("fonts.dir"): + return base + return cand + + +srv = fontgen.TFTPServer((host, 69), {}, timeout=60.0, retries=10, + resolver=resolve).start() +try: + while True: + time.sleep(0.5) + while srv.log: + ts, name, addr = srv.log.pop(0) + stamp = time.strftime("%H:%M:%S", time.localtime(ts)) + print(f"{stamp} {addr[0]}:{addr[1]} requested {name!r}", flush=True) +except KeyboardInterrupt: + pass +finally: + srv.stop() diff --git a/harness/mame/pcfwork/serve_index.py b/harness/mame/pcfwork/serve_index.py new file mode 100644 index 0000000..1f95e92 --- /dev/null +++ b/harness/mame/pcfwork/serve_index.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +"""pcfwork: serve a proper font index + one candidate font over TFTP. + +Unlike serve_candidate.py (which serves the PCF bytes as the index, so the +terminal derives a garbage name), this serves a real `fonts.dir` naming the +candidate with its own internal XLFD, so the terminal's name check can pass and +the font can actually be registered/opened. + + python3 serve_index.py [font-file] [dir] [bind-ip] +""" +import os +import sys +import time + +ROOT = "/home/arti/code/k-space/vt1200" +sys.path.insert(0, os.path.join(ROOT, "tools")) +import fontgen # noqa: E402 + +cand_path = sys.argv[1] +xlfd = sys.argv[2] if len(sys.argv) > 2 else "-misc-vt1200test-medium-r-normal--8-80-75-75-c-80-iso8859-1" +font_file = sys.argv[3] if len(sys.argv) > 3 else "v1200a.pcf" +fontdir = sys.argv[4] if len(sys.argv) > 4 else "69" +host = sys.argv[5] if len(sys.argv) > 5 else "192.168.99.1" + +with open(cand_path, "rb") as fh: + cand = fh.read() +index = f"1\n{font_file} {xlfd}\n".encode() +print(f"index={index!r}", flush=True) +print(f"font {font_file} -> {os.path.basename(cand_path)} ({len(cand)}B)", flush=True) + + +def resolve(name): + if name == fontdir or name == fontdir + "/": + return cand # the path object: the terminal reads it first + if name.endswith("fonts.dir"): + return index + if name == font_file or name.endswith("/" + font_file) or name.endswith(font_file): + return cand + return None # anything else: honest "file not found" + + +srv = fontgen.TFTPServer((host, 69), {}, timeout=60.0, retries=10, + resolver=resolve).start() +try: + while True: + time.sleep(0.5) + while srv.log: + ts, name, addr = srv.log.pop(0) + stamp = time.strftime("%H:%M:%S", time.localtime(ts)) + print(f"{stamp} {addr[0]}:{addr[1]} requested {name!r}", flush=True) +except KeyboardInterrupt: + pass +finally: + srv.stop()