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.
The terminal broadcasts an RFC 951 BOOTP request at power-up. Add an opt-in
responder that answers it with yiaddr 192.168.99.2 and siaddr 192.168.99.1,
echoing htype/hlen/xid/chaddr. It offers no boot file: the request's sname and
file are empty, so the reply's are too.
Off by default -- a bare 'serve.py 192.168.99.1' behaves exactly as before
(TFTP on port 69, nothing else). Enable with --bootp.
Delivery note: the host IP stack never receives the source-0.0.0.0 broadcast,
so the responder attaches to the MAME tap's L2 (AF_PACKET) and writes the reply
frame back. See harness/BOOTP_REPORT.md for the evidence and the still-open
'User settings invalid' / 'Disk full' symptoms.
The terminal's settings NVR was not modelled: 0x6000000 fell inside the
video-memory mapping (0x04000000..0x1fffffff), so NVR reads returned stale
framebuffer words and writes were lost. That is the owner's 'User settings
invalid' plus 'settings do not persist' (and the font path is stored here).
Map a persisted NVRAM over the 0x800-word settings block; MAME now saves and
reloads it (nvram/vt1200/nvram), verified write->restart->read. No MAME-tree
change.
The failing loopback case is a negative test: its TX buffer deliberately has
no FCS, and the test expects a CRC error. The DTCR=1 case that does carry an
FCS is delivered intact and passes, so the DMA is not dropping four bytes.
The sole remaining mismatch is one bit: the test wants RMD1=0x6B00
(ERR|FRAM|CRC|STP|ENP) and MAME's am79c90 reports 0x4B00 -- it never sets
RMD1_FRAM. No MAME-tree change made; reported for a second reader.
Add the Am7990 Technical Manual and Am79C90 datasheet to
reference/lance-datasheet/ and a report, reference/lance-loopback-fcs.md,
that answers whether MAME's am79c90 receive() FCS condition
(!(MODE_LOOP) || MODE_DTCR) is inverted.
Verdict: MAME is right. The CRC generator and checker share one circuit,
so with LOOP=1 the receiver can only check the FCS when DTCR=1 -- exactly
what MAME does. The proposed inversion would be wrong in both loopback
cases. No MAME change made.
Milestone 1 of docs/superpowers/plans/2026-10-16-lance-ethernet.md.
Modelled:
* MAME am79c90 (AM79C90) at the firmware's two windows -- bit 0x5900000 is
RAP, 0x5800000 is RDP; every other offset in those windows still falls
through to the board model.
* the Ethernet address ROM at bit 0x6800000, as EIGHT bytes. The plan said
six; the firmware's reader (0x3FDD7520) actually loops i = 0..7 and then
validates a folded ones-complement checksum over bytes 0..5 against
bytes 6..7 (0x3FDD78B0). For the MAC 08:00:2b:16:4b:5a that is 0xC186, so
the ROM is 08 00 2b 16 4b 5a c1 86. A six-byte ROM returns expected = 0
and fails LEVEL1/2 ERROR -> ETHERNET ROM CHECKSUM - 23. The checksum
bytes are derived from the firmware's own algorithm (documented in
LANCE_REPORT.md); they are not read off a real PROM.
* the interrupt on INT1, matching the ISR 0x3FA93400 and INTENB bit 1.
* bus-master DMA, translating the LANCE's byte address to the TMS34010's
bit-addressed main RAM (byte B -> word B>>1), cross-checked against the
firmware's own CSR1 = 0xFF60 = RAM word 0x7FB0.
* VT1200_LANCELOG to trace the register/ROM accesses.
Observed with the firmware's own diagnostics menu (driven by the real LK401
model; new menu_keys.lua): the M -- ETHERNET ROM TEST now PASSES. The
C -- LANCE TEST runs the chip for real -- the firmware's init block
(mode 0x004C = LOOP|INTL|DTCR, PADR 08:00:2b:16:4b:5a) is read via the new DMA,
INIT completes (CSR0 = 0x6FE3, IDON), the chip transmits and receives -- but its
internal-loopback receive still reports a CRC error under MAME (RCV descriptor
1 = 0x4B00, message count 0). Cause: MAME's receive() runs the FCS check when
MODE_LOOP && MODE_DTCR (am79c90.cpp:263) even though DTCR means no FCS was
appended. No MAME change was made; quoted and reported in LANCE_REPORT.md.
The OS (skip gate) reads the ROM MAC, programs CSR1/2/3, INITs, sees IDON,
STRTs and polls CSR0; the Terminal Manager now shows Ethernet Address
08-00-2B-16-4B-5A (was 00-00-00-00-00-00) and LAT_08002B164B5A.
Baselines unchanged: no-env samples=198 (3 PCs), VT1200_IO_PATH_STUB samples=53
(75 PCs). ROM md5 unchanged. No MAME tree changes.