Files
vt1200/dwt/notes/GUI_APPS.md
T
SDD a94e99579f dwt/gui: injected X window works via create_process
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).
2026-10-18 14:28:58 +03:00

23 KiB
Raw Blame History

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:

  1. XOpenDisplay("0::0") — 0x3F9C2570
  2. XCreateSimpleWindow(d, root, x, y, w, h, bw, border, background) — 0x3FD41B90
  3. XMapWindow(d, win) — 0x3FD645B0
  4. XSync(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:

  1. 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.
  2. Accept hit, auth rejects → authorise the injected client (the same user/node table the config UI edits), then the window should appear.
  3. 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

10. Transport settled: a create_process child opens a window

Verdict: H3 holds, with one payload bug on top. A process created by the firmware's own create_process connects to the "xserver" local service, completes the X11 setup handshake, creates and maps a window. The final, breakpoint-free run shows it at frame 2838: dwt/gui/evidence/xhello_window_frame2838.png (black 400x200 window at 100,100 with the WM frame/title bar, over the Terminal Manager).

The ROM is untouched (md5 b46a3e442ccb86134d2356bd70d22fd2); no driver edits. Run configuration is the documented VT1200_SKIP_LEVEL1=1 divergence, private -cfg_directory/-nvram_directory, BOOTP from harness/serve.py --bootp.

10.1 H1: the connect does reach the server accept — even from rdebug

The old probe address 0x3F58B800 was wrong: it is the epilogue of the function that creates the "remote debug" process (mmfm sp,a7,a9,a11,a13; rets), not the X accept. The local-service path is:

stage bit address
XOpenDisplay 3F9C2570
XParseDisplay → service attach by name 3FD3B3E0
service attach / lookup 3F70B060 / 3F70DB80
server-side accept callback (registration arg 1) 3F34EC50
local accept worker, 12-byte setup read 3F34F190 / 3F350170
setup read implementation 3F70C5C0
TCP accept (separate, auth-guarded) 3F58B830

Instrumentation of the old experiment (xhello called in the rdebug process) shows the connect is accepted, not rejected:

TR OPEN entry pc=3F9C2570 a14=01852020 ...
TR ATTACH entry pc=3F70B060 ... proc=003FFBD0
TR ATTACH ret a8=008D3050
TR SETUP-WRITE pre ...
TR ACCEPT-CB entry proc=00414F90          ; SERVER runs the service callback
TR REGISTER ret a8=007ADA30               ; re-registers for the next client
(no READ12)

proc=00414F90 is SERVER (confirmed from the process list). So the service table lookup, the attach and the accept callback all succeed; the failure is downstream of accept.

10.2 H2: no auth check on the local path

3F3F4680 (the "Client is not authorized" path) is never hit for a local connect. The auth/user-node table belongs to the TCP listener (3F58B830, X Server's TCP Port), not to the named service. Nothing rejects "0::0".

10.3 What the rdebug process does not get

After the accept callback, the server's local accept worker must dequeue the connection and read the 12-byte X11 setup. In the rdebug-process run it never does — there is no READ12 after the injected ACCEPT-CB; the client then sits in Xlib's select (3FD969F0), which is the original observation. The server side was alive (boot clients' own READ12 calls occur earlier in the same log).

A fresh create_process child is dequeued normally. Why the rdebug context is not dequeued was not reduced to a single ROM check; what is measured is the difference in outcome: same payload, same service, same server, only the calling process differs.

10.4 H3: create_process makes it work

dwt/gui/xlaunch.asm (128 B, bit 01860000) is the launcher. Its call shape is the Telnet handler's (0x3F6D4340–0x3F6D4440):

create_process(entry, name, stack, 1, arg)      ; entry = 01840000, stack = 6000

It is invoked by the real rdebug jump; create_process returns PCB 003FFBD0 and the child's xhello runs. The trace then shows the full handshake:

TR CREATE_PROC entry entryarg=003F30DC ... proc=004043D0
TR OPEN entry ... (child)  TR ATTACH ret a8=008D3050
TR ACCEPT-CB entry proc=00414F90
TR READ12 ret a8=0000000C                 ; server read the 12-byte setup
TR SRV-WRITE ret a8=00000008              ; setup reply header
TR SRV-WRITE ret a8=00000090              ; vendor + screen list
TR XREAD ret a8=00000008 expect=00000008  ; client read both
TR XREAD ret a8=00000090 expect=00000090
TR CreateSimpleWindow
TR MapWindow
TR XSync

Raw excerpt: dwt/gui/evidence/transport_evidence.txt; full trace: dwt/gui/run12/debug.log (instrumented) and dwt/gui/run13/mame.log (breakpoint-free).

10.5 The second bug: do not reload Display * from the marker cell

With H3 alone the child still crashed in XCreateSimpleWindow. The cause is in xhello, not in the transport: XOpenDisplay really returns 0x7C7290 (DISP-RET2 a8=007C7290), but the value read back from the old RAM marker cell was 0x006B7290. The root lookup then dereferenced the wrong block (all 0x5555 fill) and produced garbage; the call went wild.

The fix, in dwt/gui/xhello.asm, is to keep the Display in a11 and the window in a10 — both are callee-saved by every Xlib entry point (mmtm sp,a5,a7,a9,a10,a11,a13) — instead of reloading it from RAM, and to re-run setf 16,0,0; setf 32,0,1 after every Xlib call because Xlib changes the CPU's field state. The markers are diagnostic only now.

10.6 Reproduce

./dwt/gui/build.sh dwt/gui/xhello.asm  01840000   # 390 B
./dwt/gui/build.sh dwt/gui/xlaunch.asm 01860000   # 128 B

# boot with the private cfg/nvram and the breakpoint-free probe
VT1200_SKIP_LEVEL1=1 MSG_TRIGGER=$PWD/trigger MSG_DIR=$PWD \
  /home/arti/code/k-space/mame0289/vt1200 vt1200 \
  -rompath harness/mame/roms -sound none -skip_gameinfo \
  -networkprovider taptun -video none -nothrottle \
  -cfg_directory $PWD/cfg -nvram_directory $PWD/nvram \
  -autoboot_script $PWD/probe_win.lua -autoboot_delay 0

# after 192.168.99.2:771 answers (one rdebug connection per poke/jump)
python3 dwt/visual/inject.py --host 192.168.99.2 \
  --poke-file 0x01840000 --file dwt/gui/build/xhello.bin
python3 dwt/visual/inject.py --host 192.168.99.2 \
  --poke-file 0x01860000 --file dwt/gui/build/xlaunch.bin \
  --jump 0x01860000 --jump-wait 8

probe_win.lua contains no bpset/wpset; the final frame is produced by the payload alone. Stage markers: 01890140 reaches 6, 01890160 = 4.

10.7 Limits

  • The demonstration depends on the VT1200_SKIP_LEVEL1=1 divergence (as did the original experiment); it is evidence about the OS/X server, not a real POST.
  • The window is an empty black XCreateSimpleWindow (400x200, border 2) on the root window. The WM frames it; no text is drawn. Adding text needs XLoadFont/XCreateGC/XDrawString on the same working connection.
  • The rdebug process's own connect remains unusable; use create_process.