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.
This commit is contained in:
arti committed 2026-10-17 14:14:07 +03:00
1 parent 3a51b95317
commit 307df67f9d
8 files changed
+665

No files matched your search

+274
View File
@@ -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 <table>` 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.
+56
View File
@@ -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("<I", data, off)[0]
if (fmt & 0xFFFFFF00) != 0x100:
raise SystemExit(f"METRICS format 0x{fmt:x} is not compressed (0x100)")
# compressed-metrics body: format CARD32, then count INT16, then INT8 fields
data[off + 4], data[off + 5] = data[off + 5], data[off + 4]
le = os.path.join(HERE, "baseline_le.pcf")
with open(le, "wb") as fh:
fh.write(data)
print(f"wrote {le} ({len(data)} bytes)")
evil = fontgen.set_num_chars(bytes(data), 0x40000001)
evil_path = os.path.join(HERE, "baseline_le_evil.pcf")
with open(evil_path, "wb") as fh:
fh.write(evil)
print(f"wrote {evil_path} ({len(evil)} bytes), BITMAPS num_chars=0x40000001")
if __name__ == "__main__":
main()
+49
View File
@@ -0,0 +1,49 @@
-- pcfflow.lua -- coarse call-graph trace of the font path (no per-iteration spam).
local machine = manager.machine
local dbg = machine.debugger
local cpu = machine.devices[':maincpu']
if not dbg then print("ERROR: no debugger"); machine:exit(); return end
FRAME = 0
local function bp(addr, what)
dbg:command(string.format('bpset %s,,{ printf "%s fr=%%d\\n", FRAME ; g }', addr, what))
end
-- Font-file loaders / openers.
bp('3f9629e0', 'OPEN-cache-call')
bp('3f962af0', 'OPEN->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'
+91
View File
@@ -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'
+45
View File
@@ -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 <candidate.pcf> [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 <candidate.pcf> [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
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env bash
# Run one candidate through a proper fonts.dir index.
# run_index.sh <candidate.pcf> <XLFD> [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 <candidate.pcf> <XLFD> [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
+52
View File
@@ -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
'<dir>/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 <candidate.pcf> [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()
+54
View File
@@ -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 <candidate.pcf> <XLFD-name> [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()