hostload.py drives one rdebug connection: jump(malloc) for the COFF buffer,
poke the COFF, poke the loader at 0x01880000, write the descriptor, call the
loader, and verify status 0 plus app evidence (marker cells or xhello stage 6
and a VRAM screenshot). --tftp uses the existing tftpc.bin fetch instead of a
host poke. Adds evidence for two/three fresh boots and appends the integrate
section to dwt/notes/LOADER.md, including the bit-offset result-block layout
and the create_process clobber of the descriptor.
The firmware's tftp_open hangs in its resolver from the rdebug process, so
the payload speaks RFC 783 over the firmware socket API: socket(2,2,0),
sendto(RRQ), recvfrom(DATA)/ACK into a malloc'd buffer, close. Verified
live: 600 B fetched from 192.168.99.1 into the malloc buffer, server RRQ
logged, and rdebug peek matches probe.bin byte-for-byte.
Notes: dwt/notes/TCPIP.md (function table, ABI, limits) and the
loader/transport hand-off in dwt/notes/LOADER_CROSS.md.
The xserver local service is reachable from a payload, but a connect made
inside the rdebug process is accepted at the service layer and then never
dequeued: the server's accept callback runs (3F34EC50), but no 12-byte X11
setup read follows and Xlib waits in select.
create_process(entry,name,stack,1,arg) from a new process fixes it. xlaunch
creates the child; the server reads the setup (READ12 ret 0x0C), writes the
reply (8 + 0x90), and xhello creates/maps/syncs a 400x200 window. The payload
also had to keep Display* in a11 (callee-saved) instead of reloading it from a
RAM cell, and re-run SETF after each Xlib call.
Final breakpoint-free frame: dwt/gui/evidence/xhello_window_frame2838.png.
ROM untouched (b46a3e442ccb86134d2356bd70d22fd2).
Static: the VT1200 GUI is an X11 dix server in ROM plus Xlib clients
(SERVER, SESSIONMGR/Terminal Manager, WM, VTE). Internal clients use the
local service "xserver" via the display string "0::0"; TCP 6000 is a
separate authorised listener. Function table and the 4-call hello-window
sequence are in dwt/notes/GUI_APPS.md.
Dynamic: a poke+jump payload proved the firmware ABI (strlen=4) and then
called XOpenDisplay; it never returns (stage 2, display 0, X-server client
watermark unchanged) and the rdebug service dies. Verdict: hard for a
self-contained window; shared-RAM UI mutation (opcode 8) is the working
route.
The firmware transforms the reply (16-round Feistel per 8-byte record) and
compares it with the host-order copy of the challenge, so the answer is
swap4(inverse_transform(swap4(challenge))). The font stages are memcpy in
this path because dwt_prf initialises only mode 1.
challenge.py implements the transform and its inverse using the firmware's
fixed key/SP table; solve.py answers the challenge over TCP using the
unmodified exploit client. 320 forward vectors, the firmware's own inverse
and three live sessions validate it. A live run with no breakpoints poked
the 82-byte hello-world payload and ran it: MAGIC DEAD/BEEF/0400, VRAM
1024/1024. Notes and evidence under dwt/notes and dwt/evidence.
An 82-byte payload written in GSPA assembly, linked at bit 01800000h and
loaded into main RAM by harness/mame/hello.lua, executes on the CPU: it
writes DEAD/BEEF and a 1024-iteration counter into RAM and fills a 256x64
block in video RAM, which appears in the frame dump at x 256..511,
y 136..199. Entry is a debugger breakpoint on the level-0 menu idle loop,
not a delivery mechanism. Includes the build script, a COFF .text
extractor (gsprom's hex addresses wrap at 64 KB), the run log, RAM excerpt,
frame crops including a no-load control, and HELLO_WORLD_REPORT.md.
The 16-bit-displacement jump (low byte 0x00, two words) is JRcc, not the
three-word JAcc (low byte 0x80). Printing it as ja made linear
disassembly report the wrong control flow and made every such branch grow
by a word when reassembled.
Register 15 is the stack pointer in either file: 0x0F is A15 and 0x1F is
B15. The disassembler printed both as sp, hiding B-file stack-pointer
code. Print b15 when the R bit selects the B file.
Validated with the 1987 TI GSPA/GSPLNK/GSPROM round trip: the 32 KB region
at bit 3F208000 still reassembles byte-for-byte (md5
43adb6669571041985f3e26fd9ff7cb9), as do the 4 KB and 16 KB spans and the
0x8000/3F240000 region. The converter no longer needs its ja->jr or
sp->b15 rewrites (5 fixups -> 3); a whole-ROM diff contains only those two
output categories. The ROM is unchanged
(b46a3e442ccb86134d2356bd70d22fd2).
The 1987 DOS toolchain (GSPA/GSPLNK/GSPROM) runs under DOSBox and
reassembles 32 KB of the v2.2 ROM byte-for-byte after five documented
fixups to smart34010 output. The 1990 r5.01 tools are OS/2 NE and do
not run in DOS. Adds the converter, round-trip script, and the
DOSBox/dosemu benchmark.
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.
Isolated-netns test setup (a private tap-mess-0-0 so a second MAME can run
without holding the owner's tap), the one-file/one-index TFTP servers, the
trace/probe run wrappers, and the two server logs quoted in
FONT_STORAGE_REPORT.md.
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.