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.
17 KiB
How the VT1200's GUI apps are built — and what a payload can do with that
Status: static architecture DONE; injection verdict measured, not assumed.
A payload can call ROM routines with the firmware calling convention (proved
live: strlen("0::0") returned 4), but XOpenDisplay from the injected process
never completes: the run stays at stage 2, the display pointer stays 0, the
X server's client watermark does not move, and the rdebug service stops answering.
Creating a new X window from a payload is therefore hard, not medium.
All work is static (Ghidra on vt1200_v22.bin) plus one live experiment on a
private NVRAM/cfg under the documented VT1200_SKIP_LEVEL1=1 divergence. ROM
untouched (md5 b46a3e442ccb86134d2356bd70d22fd2), no driver edits.
Address convention used here: Ghidra byte address and firmware bit address
(= Ghidra × 8). Ghidra address = 0x7E40000 + ROM file offset.
1. The model in one paragraph
The terminal runs a small process-per-application OS on one TMS34010. RAM is
shared and unprotected; processes are contexts (a PCB from create_process, a
stack, a private file table at *(0x1B3900)+0x100). The graphics system is a
real X11 server in ROM (dix, the DEC/Athena server: strings
Hello, this is the X server, CreateWellKnownSockets,
Internal error in ConfigureWindow, BadWindow - parameter not a Window). The
GUI applications are X clients built on Xlib, which is also in ROM
(XConnectDisplay, Xlib: sequence lost, XCreateCursor-class request
builders). There is no separate DWT window API: the "DWT Window Manager" is
an ordinary X client that speaks the X11 WM protocol plus DECwindows atoms
(DEC_WM_HINTS, WM_MOVED, DEC_WM_ICON_STATE, DEC_WM_TAKE_FOCUS).
Because RAM is shared, app state (the Messages list, window ids, cursors) is
global and a routine in one process can update another app's data — that is why
rdebug opcode 8 can write the Terminal Manager's Messages box without a
connection of its own. The X protocol traffic, however, is done by each app's
own process through its own connection (Display * in a global: TM @0xB78E0,
WM @0x27AA80).
2. Function table
2.1 Processes / app lifecycle
| what | Ghidra | bits | notes |
|---|---|---|---|
create_process(entry,name,stack,1,arg) |
0x07EE0D48 |
0x3F706A40 |
mallocs a 0x39-byte PCB, copies the name, allocates the stack, links it at @0x1B3860 |
| current-process pointer | — | 0x1B3900 |
+0x100 = user area (fd table) |
| process list head | — | 0x1B3860 |
|
Main Program (creates SERVER + SESSIONMGR) |
0x07E69B6A…0x07E69C1A |
0x3F34DB50 |
itself a created process (name Main Program) |
| X server entry SERVER | 0x07E6C522 |
0x3F362910 |
dix main; prints Hello, this is the X server |
| Session Manager / Terminal Manager SESSIONMGR_PROC | 0x07E5CDBC |
0x3F2E6DE0 |
is the Terminal Manager (see §3) |
| Window Manager WM | 0x07F5702C |
0x3FAB8160 |
DWT WM; prints DWT Window Manager is here to stay |
| terminal emulator VTE | 0x07E9B60A |
0x3F4DB050 |
DECterm/VTE |
create_process argument order (from the pushes at 0x7E69B72): the first
argument is the entry point (bit address), the second the name string,
the third the stack size, the fourth a constant 1, the fifth a pointer/arg.
Example (main, 0x07E69BE2):
07E69BC8 MOVK 0x1,A9 ; arg3 = 1
07E69BCC MOVI 0x4E20,A9 ; arg2 = 0x4E20 stack
07E69BD2 MOVI 0x3F34E2E0,A8 ; arg1 = "SESSIONMGR_PROC"
07E69BDA MOVI 0x3F2E6DE0,A9 ; arg0 = entry
07E69BE2 CALLA 0x07EE0D48 ; create_process
2.2 Xlib (client side, all in ROM)
The request-builder opcode is visible in each function (MOVB 0xNN into the
request buffer), which is how these were named; the DB26 XDrawString and DB25
XCreateSimpleWindow shapes were then checked against their argument blocks.
| Xlib function | Ghidra | bits | shape seen |
|---|---|---|---|
XOpenDisplay(name) |
0x07F384AE |
0x3F9C2570 |
mallocs 0x4D8, parses the name, connects |
XParseDisplay / service lookup |
0x07FA7538 |
0x3FD3A9C0 |
splits host:display.screen, resolves "xserver" |
XCreateSimpleWindow(d,p,x,y,w,h,bw,border,bg) |
0x07FA8372 |
0x3FD41B90 |
opcode 1, 9 args |
XMapWindow(d,w) |
0x07FAC8B6 |
0x3FD645B0 |
opcode 8, 2 args |
XMapSubwindows(d,w) |
0x07FAC97E |
0x3FD64BF0 |
opcode 9 |
XSelectInput(d,w,mask) |
0x07FB2FAA |
0x3FD97D50 |
ChangeWindowAttributes, mask 0x800 |
XChangeWindowAttributes |
0x07FA70EE |
0x3FD38770 |
opcode 2 |
XConfigureWindow |
0x07FA74A2 |
0x3FD3A510 |
opcode 12 |
XInternAtom(d,name,only_if_exists) |
0x07FABC84 |
0x3FD5E420 |
opcode 16 |
XChangeProperty |
0x07FA6E66 |
0x3FD37330 |
opcode 18 |
XCreateGC(d,drawable,mask,values) |
0x07F32E00 |
0x3F997000 |
opcode 55; template copied from ROM 0x2317D0 |
XChangeGC |
0x07F333F0 |
0x3F999F80 |
opcode 56 |
XSetForeground(d,gc,pixel) |
0x07FB334C |
0x3FD99A60 |
caches in GC, marks dirty |
XSync(d,discard) |
0x07FB3ED6 |
0x3FD9F6B0 |
sends GetInputFocus (43), waits |
XDrawString(d,drawable,gc,x,y,s,n) |
0x07FB4000 |
0x3FDA0000 |
opcode 74 (PolyText8) |
XLoadFont(d,name) |
0x07FA95D0 |
0x3FD4AE80 |
opcode 45 (OpenFont) |
XCopyArea |
0x07FA7C44 |
0x3FD3E220 |
opcode 62 |
| internal PolyFillRectangle fast path | 0x07F33D1E |
0x3F99E8F0 |
opcode 70; not a public entry point |
strlen |
0x07F68B1A |
0x3FB458D0 |
used as the ABI check |
2.3 X server internals that matter
| what | Ghidra | bits |
|---|---|---|
| connection setup / accept entry | 0x07EB1700 |
0x3F58B800 |
auth reject logger (0x07E7E8D0) |
0x07E7E8D0 |
0x3F3F4680 |
| client table (32 × 0x80) | RAM | 0x1B6600 |
| client watermark | RAM | 0x1B8620 |
| client bitmap (32 bits) | RAM | 0x1B86A0 |
| well-known socket / service name | ROM "xserver" |
0x3F591F08 |
The server's connection path reads the 12-byte X11 setup (0xEBB1CE(conn,12,buf)),
checks the byte-order byte, then reads the auth name/data and looks the peer up
in the user/node table. Failure goes to a routine that logs
Client is not authorized to access server and closes.
3. Terminal Manager: what it is and how it starts
The Terminal Manager is not a separate application. It is the UI half of the
process main() creates as SESSIONMGR_PROC (entry 0x3F2E6DE0). The single
function that begins at 0x7E5CDBC runs straight through the display setup
(0x7E5D0F0), the TM window creation (0x7E5D2D0), the WM launch
(0x7E5D760) and the VTE launch (0x7E5E676). This is continuous code: the
disassembly has no return between 0x7E5CDBC and 0x7E5F7xx.
Startup, quoted:
07E5D1FA MOVI 0x3F30DC00,A8 ; "0::0" at ROM 0x7E61B80
07E5D204 CALLA 0x07F384AE ; XOpenDisplay("0::0")
07E5D20A MOVE A8,@0xB78E0 ; TM's Display * (global)
07E5D212 JRNE 0x07E5D228
07E5D216 MOVI 0x3F30DC28,A8 ; "DWTM Failed to open display\n"
07E5D21E CALLA 0x07F6683C ; printf
The title string Terminal Manager V2.2 is at ROM 0x7E61D8B
(bit 0x3F30EC58) and is referenced from the same function (0x7E5E928).
Terminal Manager XIO error... restarting... (0x7E61B53) confirms the TM is an
XIO/Xlib client, not a private drawing API.
The TM's first window is a 1×1 "WM protocol" window; the visible TM box is built
with the widget toolkit (the code at 0x7E7F24A, 0x7E8465A, 0x7E873FE,
0x7E94880, … that calls XCreateSimpleWindow + XMapWindow).
Launched from: main(), not a menu or an init list. A menu does exist for
secondary apps — the Create menu (Terminal Window on Host Port, TELNET Terminal Window…, PRINT) calls create_process at 0x7E91BA0/0x7E91C26.
4. Minimum "hello window" call sequence
From the TM's own code (0x7E5D2B4–0x7E5D2D0) and the WM's
(0x7F57044–0x7F5704E). The calling convention is not "push on SP": the
caller reserves one slot with MOVE A14,-*SP,1, pushes arguments upward
with MOVE arg,*A14+,1 (last argument first), and the callee restores A14/SP.
; root = *(*(display+0x480)+0x40) (TM 0x7E5D290..0x7E5D2AA)
MOVE @0xB78E0,A11 ; display
MOVE @0xB7F80,A9 ; default screen number
SLL 0x9,A9 / SLL 0x7,A7 / ADD A7,A9 ; * 0x280 (screen struct)
MOVE *A11(0x480),A7 / ADD A9,A7 ; screens[]
MOVE *A7(0x40),A9 / MOVE A9,@0xB7960 ; root window
; window = XCreateSimpleWindow(display, root, 0,0, 1,1, 0, 0, 1)
MOVE A14,-*SP,1
MOVK 1,A7 / MOVE A7,*A14+,1 ; background = 1
SUB A7,A7 / MOVE A7,*A14+,1 ; border
MOVE A7,*A14+,1 ; border width
MOVK 1,A7 / MOVE A7,*A14+,1 ; height
MOVE A7,*A14+,1 ; width
SUB A7,A7 / MOVE A7,*A14+,1 ; y
MOVE A7,*A14+,1 ; x
MOVE A9,*A14+,1 ; parent = root
MOVE A11,*A14+,1 ; display
CALLA 0x07FA8372 ; -> window id
The full minimum is 4 Xlib calls:
XOpenDisplay("0::0")—0x3F9C2570XCreateSimpleWindow(d, root, x, y, w, h, bw, border, background)—0x3FD41B90XMapWindow(d, win)—0x3FD645B0XSync(d, 0)—0x3FD9F6B0(flush + round trip)
To draw text add XLoadFont(d,"fixed") (0x3FD4AE80), XCreateGC
(0x3F997000) with GCFont=0x4000, XSetForeground (0x3FD99A60) and
XDrawString (0x3FDA0000). No event loop is needed for a static window; the
server keeps it after the client goes idle.
Globals: only the Display * is per-app. There is no global "current
display" to initialise; XOpenDisplay allocates and returns it. Window ids,
GCs and fonts are server-side resources and are valid for any client.
5. The injection experiment (run) and the verdict
Payload: dwt/gui/xhello.asm → dwt/gui/build/xhello.bin (390 B), linked at bit
0x01840000, built with the TI SDK flow (dwt/gui/build.sh). It first calls
strlen("0::0") as an ABI check (must return 4; if not it returns without
touching Xlib), then XOpenDisplay, root lookup, XCreateSimpleWindow,
XMapWindow, XSync, storing a stage counter at bit 0x01850140 and the
display/window at 0x01850100/0x01850120.
Run: VT1200_SKIP_LEVEL1=1, private -nvram_directory/-cfg_directory,
-networkprovider taptun, BOOTP/TFTP from harness/serve.py --bootp,
-nothrottle; frame/marker probe dwt/gui/probe2.lua. Injected with
dwt/visual/inject.py (poke + jump, challenge answered by
dwt/challenge.respond). The terminal booted to the Terminal Manager, answered
rdebug, and the payload was poked and jumped with no breakpoints.
Result:
strlen = 4 <- the firmware calling convention works
stage = 2 <- reached the XOpenDisplay call
display = 0 <- never returned
window = 0
xclient watermark = 1 (unchanged: no new client slot was taken)
jump: NO RETURN within 20 s; the same connection then times out
The machine stays alive (frames advance, the TM keeps running) but the
remote-debug process sits in Xlib's select wrapper (0x3FD969F0, waiting on
the display fd) and the rdebug service stops answering. No window appeared in
the frame dump (dwt/gui/run/gui_frame15571.png is the normal TM).
Verdict: HARD for "a payload creates its own window through XOpenDisplay".
The window API is ordinary Xlib — the missing piece is the local transport
handshake: a connection opened from the injected process is never accepted by
the server (client watermark does not move), so Xlib waits forever. The one
thing that decides it is whether the "xserver" service connection can be
established from a process other than the boot-time clients; that is not an API
question and is not answered by the ROM alone.
MEDIUM for a narrower goal: influencing an existing window. The shared-RAM
UI path is proven — rdebug opcode 8 calls the TM's append routine
0x3F309350 and the text appears in the Messages pane with no connection of its
own. Any payload can call the same TK/UI routines, but they mutate the owning
app's data structures so that the owner does the X drawing.
6. Internal IPC to the X server
Both boot-time clients open the display with the literal string "0::0"
(TM at ROM 0x7E61B80, bit 0x3F30DC00; WM at 0x7F59D29, bit 0x3FACE948).
XParseDisplay (0x7FA7538) does not build a TCP address. It resolves a
named local service:
07FA766C CALLA 0x07F65C68 ; kernel_lock (per-process critical flag)
07FA7672 MOVI 0x3FD3DCA0,A8 ; "xserver"
07FA767C CALLA 0x07EE160C ; service attach by name
07FA7688 JREQ ... ; null -> unlock, sleep 100 ms, retry
0x7EE160C is a service-table lookup/attach; the lookup itself
(0x7EE1B70) scans a 32-entry table at 0x1E8D70 (4-byte entries) with
strcmp against the name. The X server registers the same name on its side
("xserver" appears next to CreateWellKnownSockets at 0x7E6A4B0, and next to
the TCP accept X Server's TCP Port at 0x7E591F08).
So the internal route is: named local service xserver, reached by
XOpenDisplay("0::0"), not TCP and not a Unix socket path. TCP port 6000 is a
separate listener; it is gated by the user/node authorisation list
(Connection request from user %s on node %s rejected, auth check
0x7E7E8D0), which is why an external xdpyinfo gets
Client is not authorized to access server while the local service clients do
not.
7. App inventory
GUI (X clients):
| process | entry (bits) | role |
|---|---|---|
SERVER |
0x3F362910 |
X11 dix server, well-known socket xserver + TCP 6000 |
SESSIONMGR_PROC |
0x3F2E6DE0 |
Session Manager and Terminal Manager UI (Messages, Ethernet/IP/LAT, memory bar, Customize/Print Screen panels) |
WM |
0x3FAB8160 |
DWT Window Manager: frames, title bars, icon box, DEC WM atoms |
VTE |
0x3F4DB050 |
terminal emulator window (created on demand by the TM) |
PRINT |
0x3F3E2590 → 0x7E7C4B2 |
print manager, launched from the Print Screen/Create menu |
Non-GUI processes (same create_process mechanism, for completeness):
INETSERVER, LAT server (0x3F2BD3F0), MOP server (0x3FAED480), LANCE server
(0x3F9DB9A0), TCPIP_FONTS (0x3F95EB10), resolver (0x3F5B7EF0),
Telnet (0x3F6D2F90), Route (0x3F5BF380), rtvalidate (0x3F663360),
VTE_listen (0x3F50F6A0), VTE_listen_shr (0x3F50F860), Bootp
(0x3F57B710), Inetd (0x3F587C00), domain_name (0x3F5B9510),
remote debug (0x3F59F320), TCP_UP (0x3F22F940), Terminate_tcp
(0x3F22E7F0), Main Program (0x3F34DB50), DWTM/VTE helpers.
There is no file manager, editor, clock or cardfiler in this image; the only
GUI apps are the five above, plus the TM's own modal/panel code in the
0x7E7xxxx–0x7E9xxxx toolkit range.
8. The experiment that would settle the transport question
Run the same payload but instrument the server side: breakpoint the server
accept entry 0x3F58B800 and the auth check 0x3F3F4680, log hits to a file
from a Lua frame hook (MAME's printf action did not reach stdout with
-debugger none in this run, so use a wpset/bpset action that increments a
RAM counter, or a Lua hook that samples PC every frame), then inject
xhello.bin. Three outcomes:
- Accept never hit → the service connect from a non-boot process never reaches
the server; the next step is to call
0x7EE160C("xserver")by hand under the debugger and inspect its return value / the fd it installs. - Accept hit, auth rejects → authorise the injected client (the same user/node table the config UI edits), then the window should appear.
- Accept hit, auth passes, no reply → the setup packet is wrong; compare the bytes Xlib writes from the WM's connection with ours.
A second, independent route worth trying: create_process a fresh process whose
entry is the payload (create_process(0x01840000, "GUI", 0x2000, 1, 0)), so the
X connection is made by a normal new process rather than by the rdebug
dispatcher. That isolates "wrong process context" from "wrong transport".
Not run: the owner's MAME was not running, but the shared tap
(tap-mess-1000-0 / bridge vt1200) was used only after confirming no
mame0289/vt1200 process existed; a private NVRAM and cfg directory were used
and the run was stopped afterwards. No second MAME was started while another was
live.
9. Evidence files
| path | contents |
|---|---|
dwt/gui/xhello.asm |
the payload (ABI check + 4 Xlib calls) |
dwt/gui/build.sh |
TI SDK build wrapper, output under dwt/gui/ |
dwt/gui/build/xhello.bin, .lst, .map |
assembled payload, listing, map |
dwt/gui/probe.lua, probe2.lua, probe3.lua |
frame snapshots + marker/PC telemetry + accept breakpoint |
dwt/gui/run/mame.log |
boot, markers (strlen=4, stage=2, disp=0, xclients=1) |
dwt/gui/run/gui_frame15571.png |
post-injection frame: normal Terminal Manager, no new window |