mirror of
https://git.k-space.ee/arti/vt1200.git
synced 2026-10-31 12:05:05 +02:00
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.
This commit is contained in:
1 parent
1051bf2940
commit
7efb4880f0
6 files changed
+311
No files matched your search
@@ -0,0 +1,213 @@
|
||||
# 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).
|
||||
|
||||
```sh
|
||||
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.
|
||||
@@ -70,6 +70,7 @@ Reports in this directory:
|
||||
| `LANCE_REPORT.md` | the LANCE Ethernet model: the register file, the 8-byte address ROM and its checksum, the byte↔bit DMA, and the remaining loopback/CRC blocker |
|
||||
| `LANCE_LOOPBACK_REPORT.md` | `C — LANCE TEST`: the TX buffer **does** carry a correct FCS in the passing `DTCR=1` case and deliberately carries **none** in the failing one; the residual mismatch is MAME never setting `RMD1_FRAM` |
|
||||
| `NVRAM_REPORT.md` | the battery-backed NVR (bit `0x6000000`): previously unmodelled (its address was swallowed by the video-memory mapping), now a persisted `nvram_device`; the font path lives there, the font data does not |
|
||||
| `FONT_STORAGE_REPORT.md` | the TFTP font "Disk full" abort: a capture proves the **terminal** sends TFTP error 3, and it is the firmware's `tftp_close` (`0x3F5EC860`) reporting an incomplete read, not a storage condition; the font buffers are in correctly-mapped RAM, so there is no second map overlap |
|
||||
| `RENDER_REPORT.md` | the active-pixel scanline renderer, and the geometry it derived |
|
||||
| `DUART_REPORT.md` | the DUARTs: wiring, the ISR-driven transmit, the datasheet-verified model edits |
|
||||
| `BRINGUP_LOG.md` | the whole bring-up narrative, **including withdrawn claims** — read it sceptically |
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,51 @@
|
||||
-- fontprobe.lua -- live values for the TFTP font-read capacity check.
|
||||
--
|
||||
-- Breaks inside the firmware's tftp_read (0x3f5ed230) at the point where the
|
||||
-- per-datagram payload (a9) is compared with the buffer capacity (a11), and at
|
||||
-- the error path. Prints the frame, PC, a9 (payload), a11 (capacity) and the
|
||||
-- accumulated total (dword at a13+8) so the "disk full" verdict can be read
|
||||
-- directly rather than inferred.
|
||||
--
|
||||
-- FP_FRAMES=<n> budget (default 20000)
|
||||
-- <mame> vt1200 ... -debug -debugger none -debuglog \
|
||||
-- -autoboot_script harness/mame/fontprobe.lua -autoboot_delay 0
|
||||
|
||||
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
|
||||
|
||||
-- tftp_read entry: dump the three arguments from the callee frame (a13-12/-8/-4)
|
||||
dbg:command('bpset 3f5ed290,,{ printf "TFTP-ARGS fr=%d arg1(len)=%08X arg2(buf)=%08X arg3(sock)=%08X\\n", FRAME, d@(a13-12), d@(a13-8), d@(a13-4) ; g }')
|
||||
-- capacity check, just after `cmp a11,a9` (a9 = payload, a11 = capacity)
|
||||
dbg:command('bpset 3f5eed50,,{ printf "CHK fr=%d pc=%08X payload(a9)=%08X capacity(a11)=%08X a13=%08X [%08X %08X %08X | %08X %08X %08X]\\n", FRAME, PC, a9, a11, a13, d@(a13-12), d@(a13-8), d@(a13-4), d@(a13), d@(a13+4), d@(a13+8) ; g }')
|
||||
-- error path just before the message is formatted (a9 = payload first arg, a11 = capacity second)
|
||||
dbg:command('bpset 3f5eeda0,,{ printf "BUFSIZE-ERR fr=%d a9=%08X a11=%08X\\n", FRAME, a9, a11 ; g }')
|
||||
-- ERROR code 3 being sent
|
||||
dbg:command('bpset 3f5eee50,,{ printf "SEND-ERROR-3 fr=%d\\n", FRAME ; g }')
|
||||
-- tftp_read entry, count calls
|
||||
dbg:command('bpset 3f5ed230,,{ printf "TFTP-READ-ENTRY fr=%d a14=%08X\\n", FRAME, a14 ; g }')
|
||||
-- received an ERROR packet from the peer
|
||||
dbg:command('bpset 3f5edb30,,{ printf "GOT-ERROR-PKT fr=%d\\n", FRAME ; g }')
|
||||
|
||||
local maxframes = tonumber(os.getenv("FP_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-fp")
|
||||
|
||||
dbg.visible_cpu = cpu
|
||||
dbg.execution_state = 'run'
|
||||
@@ -0,0 +1,46 @@
|
||||
-- fonttrace.lua -- step-by-step trace of one TFTP font fetch.
|
||||
--
|
||||
-- Prints the control-flow milestones inside the firmware's TFTP client
|
||||
-- (tftp_read 0x3f5ed230, tftp_open 0x3f5ebc40, tftp_close 0x3f5ec860) with the
|
||||
-- key registers, so the reason a transfer is abandoned is read off the run
|
||||
-- rather than inferred from the disassembly.
|
||||
--
|
||||
-- FT_FRAMES=<n> budget (default 20000)
|
||||
|
||||
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
|
||||
|
||||
dbg:command('bpset 3f5ed230,,{ printf "TFTP-ENTRY fr=%d a14=%08X\\n", FRAME, a14 ; g }')
|
||||
dbg:command('bpset 3f5ed3f0,,{ printf " EXPECTED->%d fr=%d\\n", a11, FRAME ; g }')
|
||||
dbg:command('bpset 3f5ed690,,{ printf " RECV ret=%08X fr=%d\\n", a8, FRAME ; g }')
|
||||
dbg:command('bpset 3f5edb20,,{ printf " OPCODE=%04X fr=%d\\n", a11, FRAME ; g }')
|
||||
dbg:command('bpset 3f5ee3d0,,{ printf " BLKREAD=%04X fr=%d\\n", a11, FRAME ; g }')
|
||||
dbg:command('bpset 3f5ee420,,{ printf " BLKCHK blk=%04X exp=%08X fr=%d\\n", a11, a9, FRAME ; g }')
|
||||
dbg:command('bpset 3f5ee720,,{ printf " ->ACK/send path fr=%d\\n", FRAME ; g }')
|
||||
dbg:command('bpset 3f5eeae0,,{ printf " SEND-RET a8=%08X a11=%08X fr=%d\\n", a8, a11, FRAME ; g }')
|
||||
dbg:command('bpset 3f5eecf0,,{ printf " COPY reclen=%08X fr=%d\\n", a9, FRAME ; g }')
|
||||
dbg:command('bpset 3f5ef090,,{ printf " READ-RET a8=%08X fr=%d\\n", a8, FRAME ; g }')
|
||||
dbg:command('bpset 3f5ec750,,{ printf "OPEN-OK fr=%d\\n", FRAME ; g }')
|
||||
dbg:command('bpset 3f5ec860,,{ printf "CLOSE fr=%d\\n", FRAME ; g }')
|
||||
dbg:command('bpset 3f5ecd10,,{ printf " CLOSE->ERROR3 fr=%d\\n", FRAME ; g }')
|
||||
|
||||
local maxframes = tonumber(os.getenv("FT_FRAMES") or "20000")
|
||||
emu.register_frame_done(function()
|
||||
FRAME = FRAME + 1
|
||||
if FRAME >= maxframes then
|
||||
print("done (" .. FRAME .. " frames)")
|
||||
machine:exit()
|
||||
end
|
||||
end, "vt1200-ft")
|
||||
|
||||
dbg.visible_cpu = cpu
|
||||
dbg.execution_state = 'run'
|
||||
Reference in new issue
Block a user