mirror of
https://git.k-space.ee/arti/vt1200.git
synced 2026-10-30 12:03:54 +02:00
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:
1 parent
3a51b95317
commit
307df67f9d
8 files changed
+665
No files matched your search
@@ -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.
|
||||
@@ -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()
|
||||
@@ -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'
|
||||
@@ -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'
|
||||
Executable
+45
@@ -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
|
||||
Executable
+44
@@ -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
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user