dwt: GUI/X architecture note; Xlib callable but XOpenDisplay hangs

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.
This commit is contained in:
SDD committed 2026-10-18 14:08:42 +03:00
1 parent c0f142300b
commit a5769d3d2e
16 files changed
+1765

No files matched your search

+30
View File
@@ -343,3 +343,33 @@ and frames in `dwt/notes/DISPLAY_MESSAGE.md`.
---
## 2026-10-18 14:10 EEST — GUI apps are X clients; a payload can call Xlib but not get a display
Facts for anyone tempted to "just create a window" (details in
`dwt/notes/GUI_APPS.md`):
* The GUI is a **real X11 dix server in ROM** (process `SERVER`, entry bit
`0x3F362910`) plus Xlib in ROM. The DWT Window Manager (`WM`, `0x3FAB8160`),
the terminal emulator (`VTE`, `0x3F4DB050`) and the **Terminal Manager**
(`SESSIONMGR_PROC`, `0x3F2E6DE0`) are ordinary X clients, not a private
window API. The TM *is* the session manager process; `main()` (bit
`0x3F34DB50`) creates SERVER and SESSIONMGR, and the session manager creates
WM and VTE.
* Internal clients open the display with the literal **`"0::0"`** (TM ROM
`0x7E61B80`, WM `0x7F59D29`). `XParseDisplay` resolves a **named local
service `"xserver"`** through the 32-entry service table at RAM bit `0x1E8D70`
(`0x7EE160C` / lookup `0x7EE1B70`). TCP 6000 is a separate, user/node
authorised listener.
* Xlib entry points (bit addresses): `XOpenDisplay` `0x3F9C2570`,
`XCreateSimpleWindow` `0x3FD41B90`, `XMapWindow` `0x3FD645B0`,
`XSync` `0x3FD9F6B0`, `XCreateGC` `0x3F997000`, `XDrawString` `0x3FDA0000`.
* **Live result:** a 390-byte payload (poke+jump, no breakpoints) called
`strlen("0::0")` successfully (returned 4) using the firmware ABI, then
`XOpenDisplay` **hung**: stage 2, display 0, X-server client watermark
unchanged, rdebug service dead afterwards. The CPU sits in Xlib's select
wrapper. So the ROM is callable, but the `"xserver"` service connect from the
injected process never reaches the server. Do not assume a self-contained X
client is "one call away".
* Shared-RAM UI mutation still works (opcode 8 / `0x3F309350`), and that is the
realistic route for making the Terminal Manager draw something.
+328
View File
@@ -0,0 +1,328 @@
# 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 |