diff --git a/development.md b/development.md index 8d8ff1b..cbe421f 100644 --- a/development.md +++ b/development.md @@ -73,7 +73,7 @@ Renderer never touches Node/Electron directly — everything goes through the `w - Deleting user data needs a confirm modal; best-effort cleanup is try/catch-and-ignore with a comment saying why it is safe to ignore. - Windows apps run through runGame()/buildEnv() — never spawn wine directly elsewhere (prefix handling, Proton env and playtime live there). - Native Linux apps run through linuxrunner.runLinuxGame()/buildLinuxEnv() the same way: HOME and the XDG folders are redirected into `{prefix}/linux_pfx/`, and bwrap (when the probe says it works) forces the same layout with mounts. Never spawn a native app elsewhere. `game.kind` is absent on old entries — that means Windows; `game.isolation` is one of four levels, resolved by linuxrunner.resolveLevel: `none` (plain run), `relaxed` (bwrap + the whole host filesystem mounted writable at sandbox.FS_ROOT) and `isolated` (bwrap with the stock read-only profile), with `auto` (default) meaning relaxed where bwrap works and none where it does not. Only `auto` may weaken itself — an explicit relaxed/isolated answers `sandbox-required` instead ("no silent downgrade"). library.js keeps exactly these four values, both when reading the registry and when a patch is saved (anything else falls back to `auto`), and the level always lands in the log and the UI. resolveLevel awaits the probe when nothing has measured yet: a shortcut boots HEADLESS, so no renderer ran `sandbox:status` — without that wait `auto` would read an empty cache and quietly pick `none` (and an explicit level would refuse a machine that can sandbox perfectly well). -- Under bwrap ONLY two things stay writable: the redirected home and the app's own folder (`writableAppDir`: inside files/apps, or the folder the app was imported from — never `/usr`, `/etc`, `/bin`… and never files/wines). Portable apps write logs and data next to themselves and a read-only app folder makes them refuse to start (Prism Launcher: "couldn't create a log file - Read-only file system"). Everything else is `EROFS` by design — at the isolated level. Relaxed is the deliberate exception and says so in the UI: `FS_ROOT` is the same filesystem, mounted writable, so "everything else" is only unreachable through the real paths. +- Under bwrap ONLY two things stay writable: the redirected home and the app's own folder (`writableAppDir`: inside files/apps, or the folder the app was imported from — never `/usr`, `/etc`, `/bin`… and never files/wines). Portable apps write logs and data next to themselves and a read-only app folder makes them refuse to start (Prism Launcher: "couldn't create a log file - Read-only file system"). That bind is written LAST in buildBwrapArgs on purpose: an app lives under the host's home and the redirected home is mounted over that very directory, so the bind used to be swallowed by it and bwrap answered `execvp : No such file or directory` — a click on Play and the window closed again (measured on 0.12.0, and again as a real exec in test/sandbox-appdir.test.js). Everything else is `EROFS` by design — at the isolated level. Relaxed is the deliberate exception and says so in the UI: `FS_ROOT` is the same filesystem, mounted writable, so "everything else" is only unreachable through the real paths. - Two pipes feed one log in spawnTracked: neither may end the stream (pipe(ws, { end: false }), closed when both sources drained, at the latest LOG_DRAIN_MS after the exit) and the write stream carries an error listener. Ending it at `exit` while a pipe still had data killed the whole launcher with ERR_STREAM_WRITE_AFTER_END. - A prefix or wine build a running app uses cannot be renamed/deleted (wine holds files open there): the guards sit next to the handlers in src/ipc/, use the games service's running list plus library.js helpers (prefixNameForGame/gamesUsingPrefix/is*InUse) and answer `{ ok:false, error:'inUse'|'hasApps', apps:[names] }` for the toast. - Long operations stream progress through the single import:progress slot as `{ phase: 'count'|'copy'|'remove', files, bytes, totalFiles, totalBytes }` (runWithProgress for deletes/imports). Emit 'count' (a treeSize walk) first, copy/remove through the fsutil helpers. An isolated hand-off has its own slot, handoff:progress (`{ phase: 'copy'|'done'|'error', name, bytes, totalBytes, notice }`), because it can start before the window exists: main.js gets the event first and, when `notice` is set (a copy above linuxrunner.STAGE_NOTICE_BYTES), puts the launcher window up so the renderer's popup can be seen; below that the copy is over before a window could paint. Copies of one file go through fsutil.copyFileProgress (streamed, throttled by progressGate, `.part` + rename). @@ -143,6 +143,7 @@ node test/lansync-consent.test.js # accept / decline / timeout gate node test/lansync-token.test.js # long-sync token expiry (sliding TTL) node test/bench-sync.js # timing: cold vs cached sync node test/linux-sandbox.test.js # bwrap argv profile + the four levels, handed paths, mode gate +node test/sandbox-appdir.test.js # the app folder stays reachable inside its own redirected home node test/linux-env.test.js # redirected HOME/XDG_* for both backends node test/linux-import.test.js # ELF/AppImage/.desktop detection + icons node test/linux-links.test.js # link opening: shim binds + mime seed diff --git a/src/sandbox.js b/src/sandbox.js index e6acd69..49890c4 100644 --- a/src/sandbox.js +++ b/src/sandbox.js @@ -146,9 +146,6 @@ function buildBwrapArgs({ ...(urlOpener && urlOpener.shim ? ['--ro-bind', urlOpener.shim, urlOpener.target] : []), // hard profile: nothing of anybody's /home is even readable ...(hideHome ? ['--tmpfs', '/home', '--tmpfs', '/root', '--tmpfs', '/mnt', '--tmpfs', '/media'] : []), - // the app's own folder stays writable (portable apps keep logs/data next - // to themselves); the rest of the read-only root does not - ...(appDir ? ['--bind', appDir, appDir] : []), // --- app data (host paths bound to a fresh HOME) --- '--bind', path.join(dataRoot, 'home'), home, '--bind', path.join(dataRoot, 'config'), xdgDir('.config'), @@ -158,6 +155,11 @@ function buildBwrapArgs({ // Wayland, PipeWire, PulseAudio and the session bus all live here ...(runUser ? ['--bind', runUser, runUser] : []), ...(etcRoot ? ['--bind', etcRoot, '/etc'] : []), + // the app's own folder stays writable (portable apps keep logs/data next + // to themselves). Last, because an imported app lives under the host's home + // and the home bind above would otherwise mount over and hide it - bwrap + // then fails with "execvp : No such file or directory". + ...(appDir ? ['--bind', appDir, appDir] : []), // --- environment --- '--setenv', 'HOME', home, '--setenv', 'XDG_CONFIG_HOME', xdgDir('.config'),