From b88cacecac05f1190f36e2e5c37508a1a2a5b947 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Sun, 23 Aug 2026 17:52:02 +0000 Subject: [PATCH] confine the prefix again after wine boots, and fix two manifest package names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two things the live install found. The manifest was refused at discovery. `apt: 'wine wine32:i386'` is two packages and an architecture suffix, and a package name may be neither — nor should it be, since `dpkg --add-architecture i386` has to happen first and no package name can express that. And Fedora's Xvfb package is `xorg-x11-server-Xvfb`, with a capital X, which a manifest package name may not have; the dnf entry is dropped rather than guessed, which turns into an instruction to the owner instead of a wrong install. More importantly: confining the prefix before wine starts is a confinement wine can undo. Observed directly — after a run, every com* link had come back with a fresh timestamp, because wine rebuilds dosdevices when it boots a prefix it thinks needs updating, and a wine package upgrade is enough to make it think so. Z: did not come back that time, but "the dangerous one happened not to regenerate" is not a property to rely on. confineAndVerify now runs again once the game is up and says so if Z: returned. Verified on the live server: Z: and the three home symlinks are gone from a prefix that had them, run/ is mode 700, nothing listens on 5907, and the RFB handshake arrives through the platform relay with a valid owner token and through nothing else. Co-Authored-By: Claude Opus 5 Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043 --- manifest.ts | 11 +++++++++-- sidecar/prefix.ts | 31 +++++++++++++++++++++++++++---- sidecar/session.ts | 7 ++++++- 3 files changed, 42 insertions(+), 7 deletions(-) diff --git a/manifest.ts b/manifest.ts index 0f5bf8f..3c3eaeb 100644 --- a/manifest.ts +++ b/manifest.ts @@ -88,13 +88,20 @@ export const manifest: PluginManifest = { { binary: 'wine', reason: 'Runs the game. Minesweeper X is a 32-bit Windows executable, so this host also needs i386 multiarch (`dpkg --add-architecture i386`) and the 32-bit wine package — a 64-bit-only wine cannot load it.', - packages: { apt: 'wine wine32:i386', dnf: 'wine', pacman: 'wine', brew: 'wine-stable' }, + // `apt: 'wine'` alone, deliberately. The 32-bit half is `wine32:i386`, and a package name may not + // carry an architecture suffix or a second package — nor should it, since `dpkg --add-architecture + // i386` has to happen first and no package name can express that. The `reason` above carries the + // whole command, which is what the owner is shown when this is missing. + packages: { apt: 'wine', dnf: 'wine', pacman: 'wine', brew: 'wine-stable' }, scope: 'owner', }, { binary: 'Xvfb', reason: 'The virtual display the game draws on. A framebuffer with no hardware behind it, so nothing appears on any real monitor.', - packages: { apt: 'xvfb', dnf: 'xorg-x11-server-Xvfb', pacman: 'xorg-server-xvfb' }, + // No `dnf` entry, and not by omission. Fedora's package is `xorg-x11-server-Xvfb` — capital X — + // and a manifest package name must be lowercase. Left out means "cannot be installed automatically + // here", which turns into an instruction to the owner rather than a wrong guess. + packages: { apt: 'xvfb', pacman: 'xorg-server-xvfb' }, scope: 'owner', }, { diff --git a/sidecar/prefix.ts b/sidecar/prefix.ts index fd0b3a1..051c528 100644 --- a/sidecar/prefix.ts +++ b/sidecar/prefix.ts @@ -115,12 +115,35 @@ export async function ensurePrefix(log: (line: string) => void): Promise void): boolean { + const z = join(WINEPREFIX, 'dosdevices', 'z:'); + const regenerated = existsSync(z); + confinePrefix(); + if (regenerated) { + // Not fatal — it has just been removed again, before anything could look at the display. Worth saying + // out loud because it means a wine upgrade landed, and the NEXT thing to check is whether the prefix + // still runs the game at all. + log('wine regenerated the Z: drive mapping during startup; removed it again'); + } + return regenerated; +} + export function confinePrefix(): void { const dosdevices = join(WINEPREFIX, 'dosdevices'); if (existsSync(dosdevices)) { diff --git a/sidecar/session.ts b/sidecar/session.ts index 65945c9..b572e30 100644 --- a/sidecar/session.ts +++ b/sidecar/session.ts @@ -1,7 +1,7 @@ import type { Subprocess } from 'bun'; import { existsSync, rmSync } from 'node:fs'; import { GAME_EXE, RUN_DIR, VNC_SOCKET, ensureDirs } from './paths'; -import { WINE_ENV, ensurePrefix, prefixReady } from './prefix'; +import { WINE_ENV, confineAndVerify, ensurePrefix, prefixReady } from './prefix'; import { run, waitFor } from './run'; /** @@ -179,6 +179,11 @@ async function startInner(log: (line: string) => void): Promise { } } + // Wine has now booted the prefix, and booting it is what can restore `Z:`. The confinement in + // `ensurePrefix` happens before that, so on its own it would be undone by exactly the event it exists to + // survive. See `confineAndVerify`. + confineAndVerify(log); + // ── the display server ── if (!alive(vnc)) { // A socket left by a crashed run refuses to bind. Removing it is safe: nothing else may write here.