Shared text clipboard via VMware backdoor + W95TOOLS.EXE guest agent (#361)

Three layers:

  v86 — src/vmware.js gains the legacy text-clipboard backdoor commands
  (GETSELLENGTH/GETNEXTPIECE/SETSELLENGTH/SETNEXTPIECE, 6–9). The host
  stages bytes via the vmware-clipboard-host bus event; the guest pushes
  via 8/9 and the device emits vmware-clipboard-guest when the buffer
  fills. Same wire protocol as open-vm-tools' pre-RPC copy/paste.
  Committed on the windows95-base fork branch; libv86.js rebuilt here.

  renderer — src/renderer/clipboard.ts polls Electron's clipboard (no
  change event exists), translates host UTF-8/LF ↔ guest CP-1252/CRLF,
  and bounces bytes through the two bus events. Echo-suppressed so a
  value we just wrote does not come back as a change.

  guest — guest-tools/agent/W95TOOLS.EXE is a 22 KB hidden-window agent
  that joins the Win32 clipboard-viewer chain (push-on-copy) and polls
  the backdoor on a 250 ms timer (pull-from-host). Win9x runs ring-3
  with the I/O bitmap wide open, so a plain IN EAX,DX from a user
  process reaches the port — no driver needed. Named for growth: time
  sync and host-initiated shutdown will live here too. Built with Open
  Watcom v2 inside Docker (Makefile + Dockerfile alongside the source);
  subsystem 4.0, no msvcrt, runs on Win95 RTM.

Install: copy \\HOST\TOOLS\agent\W95TOOLS.EXE into the guest and drop a
shortcut in StartUp. Text only, 64 KB cap.
This commit is contained in:
Felix Rieseberg
2026-04-11 20:17:06 -07:00
committed by GitHub
parent 6e73df11ae
commit bc76e9c79a
9 changed files with 322 additions and 10 deletions

View File

@@ -21,3 +21,23 @@ Install inside the guest:
absolute mouse driver**.
3. Reboot. The app detects the driver and stops grabbing pointer lock;
ESC still toggles lock for games that want raw relative input.
## agent/ — W95TOOLS guest agent
`W95TOOLS.EXE` is a hidden-window agent that talks to the emulator over
the VMware backdoor (port 0x5658). Currently it does one thing: bridges
Windows 95's `CF_TEXT` clipboard to the host (legacy backdoor commands
69; host side is `src/renderer/clipboard.ts`, which polls Electron's
clipboard). It's also where time sync, host-initiated shutdown, and a
tray icon will live when those land.
Install inside the guest:
1. Copy `\\HOST\TOOLS\agent\W95TOOLS.EXE` to `C:\WINDOWS\`.
2. Drop a shortcut to it in
`C:\WINDOWS\Start Menu\Programs\StartUp` so it runs on login.
Copy text on either side and it appears on the other within ~250 ms.
Text only; conversion is Windows-1252 ↔ UTF-8 with CRLF ↔ LF, capped at
64 KB. Built from `w95tools.c` with Open Watcom v2 — `make -C
guest-tools/agent` (needs Docker).

View File

@@ -0,0 +1,12 @@
FROM --platform=linux/amd64 debian:bookworm-slim
RUN apt-get update && \
apt-get install -y --no-install-recommends curl xz-utils ca-certificates make && \
rm -rf /var/lib/apt/lists/*
RUN mkdir -p /opt/watcom && \
curl -fsSL https://github.com/open-watcom/open-watcom-v2/releases/download/Current-build/ow-snapshot.tar.xz \
| tar -xJ -C /opt/watcom
ENV WATCOM=/opt/watcom
ENV PATH=$WATCOM/binl64:$PATH
ENV INCLUDE=$WATCOM/h:$WATCOM/h/nt
ENV LIB=$WATCOM/lib386:$WATCOM/lib386/nt
WORKDIR /work

View File

@@ -0,0 +1,30 @@
# Build W95TOOLS.EXE for Windows 95 with Open Watcom v2, inside Docker.
#
# Watcom is the only readily-available cross-compiler that emits a PE binary
# Win95 RTM will load (subsystem 4.0, no msvcrt). mingw-w64 targets NT. The
# macOS-native Watcom binaries are unsigned and Gatekeeper kills them, so we
# run the linux/amd64 build under Docker instead.
IMAGE := windows95-ow2
# Docker Desktop's bind-mount file sync races with recently-edited files; work
# around it by piping the source on stdin and building in /tmp inside the
# container, then dumping the EXE bytes back over stdout.
DOCKER := docker run --rm -i --platform linux/amd64 $(IMAGE)
CFLAGS := -bt=nt -3r -zq -wx -we -os -s
.PHONY: all image clean
all: W95TOOLS.EXE
image:
docker build --platform linux/amd64 -t $(IMAGE) .
W95TOOLS.EXE: w95tools.c image
$(DOCKER) sh -c 'cd /tmp && cat >w95tools.c && \
wcc386 $(CFLAGS) w95tools.c && \
wlink system nt_win option quiet name W95TOOLS.EXE \
file w95tools.o library kernel32,user32 && \
cat W95TOOLS.EXE' <w95tools.c >$@
clean:
rm -f W95TOOLS.EXE

Binary file not shown.

View File

@@ -0,0 +1,181 @@
/*
* W95TOOLS — guest-side integration agent for the windows95 emulator.
*
* Currently: bidirectional text clipboard. Talks to the emulator over the
* legacy VMware backdoor (port 0x5658; implemented in v86's vmware.js).
* Joins the Win32 clipboard-viewer chain so guest copies are pushed
* immediately, and polls the backdoor on a timer so host copies show up
* within ~250 ms.
*
* Win9x runs ring-3 code with the I/O bitmap wide open, so a plain IN works
* from a user process — no driver needed. On NT this would #GP; we don't run
* there.
*
* Build with Open Watcom v2 (see Makefile). Links USER32/KERNEL32 only,
* runs on Win95 RTM.
*/
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#define VMW_MAGIC 0x564D5868UL
#define VMW_PORT 0x5658
#define CMD_GETLEN 6
#define CMD_GETDATA 7
#define CMD_SETLEN 8
#define CMD_SETDATA 9
#define CMD_VERSION 10
#define POLL_MS 250
#define MAX_CLIP 0xFFFF
extern unsigned long bd(unsigned long cmd, unsigned long arg);
#pragma aux bd = \
"mov eax, 564D5868h" \
"mov edx, 5658h" \
"in eax, dx" \
parm [ecx] [ebx] \
value [eax] \
modify [edx];
extern unsigned long bd_ebx(unsigned long cmd, unsigned long arg);
#pragma aux bd_ebx = \
"mov eax, 564D5868h" \
"mov edx, 5658h" \
"in eax, dx" \
parm [ecx] [ebx] \
value [ebx] \
modify [eax edx];
static HWND g_next;
static int g_ignore;
static void push_to_host(HWND hwnd)
{
HANDLE h;
char *p;
unsigned long len, i, w;
if (!IsClipboardFormatAvailable(CF_TEXT)) {
bd(CMD_SETLEN, 0);
return;
}
if (!OpenClipboard(hwnd)) return;
h = GetClipboardData(CF_TEXT);
if (h && (p = (char *)GlobalLock(h)) != 0) {
len = lstrlen(p);
if (len > MAX_CLIP) len = MAX_CLIP;
bd(CMD_SETLEN, len);
for (i = 0; i < len; i += 4) {
w = (unsigned char)p[i];
if (i + 1 < len) w |= (unsigned long)(unsigned char)p[i+1] << 8;
if (i + 2 < len) w |= (unsigned long)(unsigned char)p[i+2] << 16;
if (i + 3 < len) w |= (unsigned long)(unsigned char)p[i+3] << 24;
bd(CMD_SETDATA, w);
}
GlobalUnlock(h);
}
CloseClipboard();
}
static void pull_from_host(HWND hwnd)
{
long len;
unsigned long i, w;
HGLOBAL h;
char *p;
len = (long)bd(CMD_GETLEN, 0);
if (len < 0) return;
if (len > MAX_CLIP) len = MAX_CLIP;
h = GlobalAlloc(GMEM_MOVEABLE | GMEM_DDESHARE, (DWORD)len + 1);
if (!h) return;
p = (char *)GlobalLock(h);
for (i = 0; i < (unsigned long)len; i += 4) {
w = bd(CMD_GETDATA, 0);
p[i] = (char)w;
if (i + 1 < (unsigned long)len) p[i+1] = (char)(w >> 8);
if (i + 2 < (unsigned long)len) p[i+2] = (char)(w >> 16);
if (i + 3 < (unsigned long)len) p[i+3] = (char)(w >> 24);
}
p[len] = 0;
GlobalUnlock(h);
if (!OpenClipboard(hwnd)) { GlobalFree(h); return; }
g_ignore++;
EmptyClipboard();
SetClipboardData(CF_TEXT, h);
CloseClipboard();
}
static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
{
switch (msg) {
case WM_CREATE:
g_next = SetClipboardViewer(hwnd);
SetTimer(hwnd, 1, POLL_MS, 0);
return 0;
case WM_DRAWCLIPBOARD:
if (g_ignore > 0) g_ignore--;
else push_to_host(hwnd);
if (g_next) SendMessage(g_next, msg, wp, lp);
return 0;
case WM_CHANGECBCHAIN:
if ((HWND)wp == g_next) g_next = (HWND)lp;
else if (g_next) SendMessage(g_next, msg, wp, lp);
return 0;
case WM_TIMER:
pull_from_host(hwnd);
return 0;
case WM_DESTROY:
ChangeClipboardChain(hwnd, g_next);
KillTimer(hwnd, 1);
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, msg, wp, lp);
}
int PASCAL WinMain(HINSTANCE hi, HINSTANCE hp, LPSTR cmd, int show)
{
WNDCLASS wc;
HWND hwnd;
MSG msg;
(void)hp; (void)cmd; (void)show;
if (CreateMutex(0, FALSE, "W95Tools") && GetLastError() == ERROR_ALREADY_EXISTS)
return 0;
if (bd_ebx(CMD_VERSION, 0) != VMW_MAGIC) {
MessageBox(0, "VMware backdoor not present.", "W95Tools", MB_OK | MB_ICONSTOP);
return 1;
}
wc.style = 0;
wc.lpfnWndProc = WndProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hi;
wc.hIcon = 0;
wc.hCursor = 0;
wc.hbrBackground = 0;
wc.lpszMenuName = 0;
wc.lpszClassName = "W95Tools";
RegisterClass(&wc);
hwnd = CreateWindow("W95Tools", "W95Tools", WS_OVERLAPPED,
0, 0, 0, 0, 0, 0, hi, 0);
if (!hwnd) return 1;
while (GetMessage(&msg, 0, 0, 0) > 0) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return 0;
}