Files
vt1200/harness/mame/FONT_STORAGE_REPORT.md
T
arti 7efb4880f0 harness: the TFTP font 'Disk full' is the terminal's own tftp_close, not a storage or map bug
A tcpdump on the tap plus a live execution trace settle the 'Disk full or
allocation exceeded' abort: the TERMINAL sends TFTP error 3, from
tftp_close (0x3F5EC860), which reports an incomplete read (the last DATA
block read was a full 512-byte block) using its fixed text for code 3. It
is not a capacity or disk condition.

The font buffers (path/index read buffer bit 0x9316D0, fos parser stream
buffer 0x227FD0, parsed-font cache 0x227F50) are all in main RAM
(0x00000000-0x01FFFFFF), mapped and writable; the writes land in RAM dumps.
No second map overlap exists, so the NVR fix did not cause this. vt1200.cpp
is unchanged and every baseline is intact.

New: fonttrace.lua, fontprobe.lua, two evidence pcaps, FONT_STORAGE_REPORT.md.
2026-10-16 22:03:16 +03:00

10 KiB
Raw Blame History

The TFTP font "Disk full" abort — who sends it, where the font goes, why it is spurious

Date: 2026-10-16. Firmware vt1200_v22.bin md5 b46a3e442ccb86134d2356bd70d22fd2 (unmodified). No MAME-tree change, and no change to harness/mame/vt1200.cpp at all. This report answers the two questions in the brief with a live tcpdump and a live execution trace, and records why the answer is not a second map overlap.

Verdict

  1. The terminal sends the TFTP error 3, not the server. The capture shows the client emitting the ERROR packet after the server's DATA packets. The server's only error path is "File not found" (code 1); harness/serve.py never sends code 3.
  2. The error is not a storage condition. It is the firmware's own tftp_close routine reporting "the connection was closed before the read reached a short (final) block" using its fixed text for error code 3, Disk full or allocation exceeded. Nothing is full.
  3. The font storage region is plain main RAM, correctly mapped. The receive buffers and the parsed-font cache all sit inside the driver's RAM mapping (bit 0x00000000–0x01FFFFFF); the writes land (verified in RAM dumps). There is no second map overlap. The NVR fix is not the cause.
  4. A valid font transfer completes and is parsed (verified). A font the parser rejects or abandons part-way — such as the crafted 25 KB font — reads only the first 1024 bytes and then closes, which is what produces the spurious "Disk full".

1. Who sends the error — capture

harness/mame/font_tftp_real.pcap (the owner's exact harness/serve.py, on the isolated tap; the server also has the same log line the owner sees):

192.168.99.2.1025 > 192.168.99.1.69: TFTP, length 11,  RRQ "69" octet
192.168.99.1.69   > 192.168.99.2.1025: TFTP, length 516, DATA block 1
192.168.99.2.1025 > 192.168.99.1.69: TFTP, length 4,   ACK block 1
192.168.99.1.69   > 192.168.99.2.1025: TFTP, length 516, DATA block 2
192.168.99.2.1025 > 192.168.99.1.69: TFTP, length 36,  ERROR ENOSPACE "Disk full or allocation exceeded"

The last packet is from 192.168.99.2 (the terminal) to the server. The server's _send_file() receives it and prints tftp: client aborted transfer: '\x00\x03Disk full or allocation exceeded'. So the earlier reading of the log's wording was right and the "the terminal prints a received error" reading was wrong: the terminal generates and sends it. (harness/mame/font_tftp_crafted.pcap shows the same for the crafted font.)

2. The firmware path that sends it

The string Disk full or allocation exceeded is the terminal's fixed message for TFTP error code 3 (ROM file offset 0x7E225, bit 0x3F5F1128); the per-code error table is built by L3F5EF610 (0x3F5EF610, the send-error routine). Code 3 is emitted from exactly one firmware site on this path:

tftp_close  L3F5EC860   (bit 0x3F5EC860)
3f5ec8a0: move @0x1cb940, a11, 1     ; last-block flag
3f5ec8d0: jreq L3F5ECD10             ; flag == 0  ->  send error 3
   ... build and send the ACK instead ...
L3F5ECD10:
3f5ecd10: move a14, -*sp, 1
3f5ecd20: movk 3, a9                 ; TFTP error code 3
3f5ecd30: move a9, *a14+, 1
3f5ecd40: move *a13(0xffe0), *a14+, 1
3f5ecd60: calla L3F5EF610            ; tftp_send_error(...)

@0x1cb940 (the "last block was short" flag) is set inside tftp_read (0x3F5ED230):

3f5ef000: cmpi 0x200, a11            ; payload == 512 (a full block) ?
3f5ef020: movk 1, a9
3f5ef030: jrne L3F5EF050
3f5ef040: sub a9, a9                 ; full block  -> 0 (more may follow)
3f5ef050: move a9, @0x1cb940, 1      ; short block -> 1 (this was the end)

So the rule is mechanical: if the connection is closed and the last DATA block read was a full 512-byte block, tftp_close sends ERROR 3. The separate "bufsize too small" check in tftp_read (0x3F5EED30–0x3F5EEDA0) never fired in any run — the payload was always ≤ the capacity.

What stops the read early

Live trace (harness/mame/fonttrace.lua, debug.log), no-env + skip level 1:

  • the font-path object 69 (served as system-10x20.pcf, 25 860 B): tftp_read reads one 512-byte block, returns 512, then tftp_close fires and takes the CLOSE->ERROR3 branch;
  • the same with baseline.pcf (680 B) served for 69: identical — one full block, then close/error;
  • a 400-byte object (single short block, <512): DATA1 → ACK1, no error;
  • the 680-byte baseline.pcf read as a font (the terminal derives a name from the path object and re-fetches): DATA1 → ACK1 → DATA2 → ACK2, no error — the read reached the short final block;
  • the crafted evil-real.pcf (25 860 B) read as a font: DATA1 → ACK1 → DATA2 → ACK2 → server sends DATA3, the terminal never ACKs it, and ~4 s later the terminal sends ERROR 3 — the parser consumed the first 1024 bytes and abandoned the font before EOF.

So the abort tracks "how much of the file the reader asked for", not any size/capacity limit of a storage device. A reader that stops on a full block (the path/index reader, one block; the crafted font's parser, two blocks) trips tftp_close; a reader that reads to EOF does not.

3. Where the terminal stores a fetched font — and its mapping

Read from the live CPU and from RAM dumps (VT1200_DUMP_RAM_FRAMES); a RAM-dump byte offset is bit address >> 3 (verified against the NVR settings copy: the font path 192.168.99.1:69 sits at byte 0x345EB, which is the settings copy base bit 0x1A0020 plus offset 0x5E7).

what bit address byte mapping
font-path / index read buffer (tftp_read arg 2) 0x9316D0 0x1262DA main RAM (0x00000000–0x01FFFFFF)
fos PCF parser stream buffer 0x227FD0 0x44FFA main RAM
parsed-font cache record array 0x227F50 0x44FEA main RAM
cache count / capacity 0x227FB0 / 0x227FC0 0x44FF6 / 0x44FF8 main RAM
settings copy (font path lives here) 0x1A0020 0x34004 main RAM

All of these are inside the driver's RAM region and are writable: the RAM dump taken during a fetch contains the received file bytes at the read buffer's address, and the settings copy contains the NVR font path. The only other addresses the font/TFTP code touches are ROM. (A scan of every absolute operand in harness/dis/fontregion-v22.txt finds 22 main-RAM addresses, 104 ROM addresses, and nothing in the VRAM range, the board windows, or unmapped space.)

The "second map overlap" lead: checked, none exists

The NVR fix repaired 0x6000000 sitting inside the video-memory mapping (0x04000000–0x1FFFFFFF). I looked for the same shape in the font path and it is not there:

  • every font/TFTP buffer is in RAM (bit < 0x02000000), far below the VRAM base 0x04000000, and the writes demonstrably land;
  • the only region the font path reads that could have been swallowed is the NVR itself, and that is now correctly mapped — the font path was read out of it intact.

The NVRAM bisect

Reverting the NVR mapping cannot change this, so I did not spend a rebuild on a confounded experiment (with the NVR unmapped the font path reads garbage, so the terminal would not even request 69, and the comparison would be meaningless). The direct evidence is stronger: the abort is driven by the served content (the 400-byte object completes; the 680-byte object aborts; the same 680-byte object read as a font completes) and by the parser's early exit on the crafted font — neither of which touches 0x6000000–0x6007FFF. The NVR fix did not introduce the abort; it is upstream of it, because it is what lets the font path be read at all.

4. Does a font fetch now complete? Does the terminal parse it?

  • Yes for a font the parser accepts. Serving baseline.pcf as the font name the terminal derives, the transfer reads to the short final block and completes with no ERROR, and the parser runs: the font's internal name vt1200test appears in RAM at byte 0x1316, and the font-cache count moves.
  • No for the crafted 25 KB font, and the reason is the parser, not storage. evil-real.pcf reads DATA1/DATA2 (1024 bytes) and the parser then abandons it; the inevitable tftp_close then reports ERROR 3. Any font the parser bails on part-way will produce the same spurious "Disk full" — including, in the owner's log, the 69 path object when serve.py returns a 25 KB font for it.

So the "Disk full" does stop close of connection, but it is downstream of the parser's decision, not a reason the font "cannot be stored". Fixing the message would not make the crafted parse succeed; the parse already stops on its own at 1024 bytes.

5. Baselines and configurations

vt1200.cpp was never changed, so every baseline is intact by construction; two were re-run on the shipped binary to confirm:

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)
ROM md5                         : b46a3e442ccb86134d2356bd70d22fd2

VT1200_SKIP_LEVEL1=1 (OS + Terminal Manager) and VT1200_MOUSE=1 are untouched (no code change).

6. What I refused to fake

  • No ROM patch; the ROM stays byte-identical.
  • No change to harness/serve.py (another worker's file) and no invented font capacity or patched firmware check.
  • No "make the error go away" shortcut: the error is reported as the terminal's own tftp_close behaviour, with the packet capture and the code path that prove it.

7. Reproduction

The tests ran in an isolated network namespace so they do not disturb the owner's tap-mess-1000-0 (a second MAME cannot open it — TUNSETIFF returns EBUSY; verified). Scratch lives in harness/mame/fontwork/ (untracked).

harness/mame/fontwork/netns_up.sh          # private tap-mess-0-0 in netns vt1test
harness/mame/fontwork/run_trace_serve.sh 25 # owner's serve.py + tcpdump + fonttrace.lua
harness/mame/fontwork/run_trace5.sh 40      # crafted 25 KB font via the font reader
harness/mame/fontwork/run_custom.sh reference/fonts/baseline.pcf 30
harness/mame/fontwork/run_custom.sh harness/mame/fontwork/small400.pcf 25

New instrumentation in this directory:

  • harness/mame/fonttrace.lua — prints the control-flow milestones of one fetch (tftp_read, tftp_open, tftp_close, the opcode/block checks, the ACK/error branch) with registers, to debug.log.
  • harness/mame/fontprobe.lua — prints the bufsize/payload values at the capacity check (0x3F5EED50) and at the error path.