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.
10 KiB
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
- 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.pynever sends code 3. - The error is not a storage condition. It is the firmware's own
tftp_closeroutine 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. - 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. - 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 assystem-10x20.pcf, 25 860 B):tftp_readreads one 512-byte block, returns 512, thentftp_closefires and takes theCLOSE->ERROR3branch; - the same with
baseline.pcf(680 B) served for69: identical — one full block, then close/error; - a 400-byte object (single short block,
<512): DATA1 → ACK1, no error; - the 680-byte
baseline.pcfread 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 base0x04000000, 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.pcfas 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 namevt1200testappears in RAM at byte0x1316, and the font-cache count moves. - No for the crafted 25 KB font, and the reason is the parser, not storage.
evil-real.pcfreads DATA1/DATA2 (1024 bytes) and the parser then abandons it; the inevitabletftp_closethen reports ERROR 3. Any font the parser bails on part-way will produce the same spurious "Disk full" — including, in the owner's log, the69path object whenserve.pyreturns 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_closebehaviour, 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, todebug.log.harness/mame/fontprobe.lua— prints thebufsize/payloadvalues at the capacity check (0x3F5EED50) and at the error path.