The 1987 TI K&R toolchain already compiled and linked a fib test, but nothing
had ever been executed on a VT1200. This is that program: hello.c draws HELLO
into video RAM (1 bpp, bit 0x04000000, no X11) and is loaded through the COFF
loader with hostload.py, on the real unit and in MAME.
Two runtime traps shaped the design, both recorded in the README:
* boot.obj's exit() is `for(;;)` and _c_int00 ends in `reti`. That is a
standalone-ROM runtime, not a process: the child spun forever, and because
the VT1200 OS is cooperative it took the whole terminal down (illegal
opcode on hardware). entry.asm replaces boot.obj -- set STK/FP, call
_main, `rets` back to the create_process trampoline, the same return
xhello uses -- and the image carries no .cinit (tables are built in code).
* Pointers are bit addresses and pointer arithmetic scales by object size,
so p + n on a char * is n bytes. The pre-fix frame in evidence/ is that
bug: 32 one-byte bars spread across a row.
Framebuffer geometry is not portable: the real unit is 1280 px wide at a
256-byte row pitch, MAME's canvas is 1024 px at 128. build.sh builds both
(ROW_BYTES), and the README records how the real pitch was measured from a
real-unit frame rather than taken from MAME.
Evidence: real-unit frames in harness/c-hello/evidence/ (the pre-fix stripes
and the working HELLO), and dwt/loader/evidence/real-unit-xhello/ for the X
window that ran on the unit earlier.
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.