authorgravatar for herial_vi@icloud.comLeonel Rivas <herial_vi@icloud.com> 2026-07-17 14:21:19-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-17 23:21:19+02:00
log1047dac0c5296b1c548e607f061f95227a706b49
tree89dc8b957857cdc21683554a69b24f9ae585d81a
parent143b9c9280015f0ff9fa70cd9e4edb18e975c94d
signature Signed by PGP key B5690EEEBB952194

fix(terminal): strip AppImage runtime env from spawned terminals (#3108)

Co-authored-by: Julius Marminge <julius0216@outlook.com> Co-authored-by: codex <codex@users.noreply.github.com>

2 files changed, 105 insertions(+), 1 deletions(-)

apps/server/src/terminal/Manager.test.ts+57
......@@ -1334,6 +1334,63 @@ it.layer(
13341334 }),
13351335 );
13361336
1337 it.effect("strips AppImage runtime env from terminal sessions", () =>
1338 Effect.gen(function* () {
1339 const appDir = "/tmp/.mount_T3Codeabc123";
1340 const { manager, ptyAdapter } = yield* createManager(5, {
1341 env: {
1342 APPIMAGE: "/home/user/T3-Code.AppImage",
1343 APPDIR: appDir,
1344 ARGV0: "/home/user/T3-Code.AppImage",
1345 OWD: "/home/user/project",
1346 PATH: `${appDir}/usr/bin:${appDir}:/usr/local/bin:/usr/bin:/bin`,
1347 LD_LIBRARY_PATH: `${appDir}/usr/lib:/home/user/.local/lib`,
1348 TEST_TERMINAL_KEEP: "keep-me",
1349 },
1350 });
1351 yield* manager.open(openInput());
1352 const spawnInput = ptyAdapter.spawnInputs[0];
1353 expect(spawnInput).toBeDefined();
1354 if (!spawnInput) return;
1355
1356 // AppImage runtime markers must never reach the PTY — tools inside the
1357 // terminal otherwise resolve against the AppImage mount (e.g. PHP_BINARY
1358 // reporting the AppImage path instead of the real binary).
1359 expect(spawnInput.env.APPIMAGE).toBeUndefined();
1360 expect(spawnInput.env.APPDIR).toBeUndefined();
1361 expect(spawnInput.env.ARGV0).toBeUndefined();
1362 expect(spawnInput.env.OWD).toBeUndefined();
1363 // PATH/LD_LIBRARY_PATH keep the user's real entries but drop the AppImage
1364 // mount segments that the runtime prepended.
1365 expect(spawnInput.env.PATH).toBe("/usr/local/bin:/usr/bin:/bin");
1366 expect(spawnInput.env.LD_LIBRARY_PATH).toBe("/home/user/.local/lib");
1367 // Unrelated host vars still pass through untouched.
1368 expect(spawnInput.env.TEST_TERMINAL_KEEP).toBe("keep-me");
1369 }),
1370 );
1371
1372 it.effect("leaves the environment untouched when not launched from an AppImage", () =>
1373 Effect.gen(function* () {
1374 const { manager, ptyAdapter } = yield* createManager(5, {
1375 env: {
1376 PATH: "/usr/local/bin:/usr/bin:/bin",
1377 LD_LIBRARY_PATH: "/home/user/.local/lib",
1378 // Without APPIMAGE/APPDIR set, OWD is an ordinary variable and must
1379 // not be stripped — only an AppImage launch gives it special meaning.
1380 OWD: "/home/user/keep-this",
1381 },
1382 });
1383 yield* manager.open(openInput());
1384 const spawnInput = ptyAdapter.spawnInputs[0];
1385 expect(spawnInput).toBeDefined();
1386 if (!spawnInput) return;
1387
1388 expect(spawnInput.env.PATH).toBe("/usr/local/bin:/usr/bin:/bin");
1389 expect(spawnInput.env.LD_LIBRARY_PATH).toBe("/home/user/.local/lib");
1390 expect(spawnInput.env.OWD).toBe("/home/user/keep-this");
1391 }),
1392 );
1393
13371394 it.effect("injects runtime env overrides into spawned terminals", () =>
13381395 Effect.gen(function* () {
13391396 const { manager, ptyAdapter } = yield* createManager();
apps/server/src/terminal/Manager.ts+48-1
......@@ -1065,6 +1065,53 @@ function shouldExcludeTerminalEnvKey(key: string): boolean {
10651065 return TERMINAL_ENV_BLOCKLIST.has(normalizedKey);
10661066}
10671067
1068// Marker variables the AppImage runtime injects into the process it launches.
1069// They describe the AppImage itself, not the user's session, so terminals must
1070// not inherit them.
1071const APPIMAGE_RUNTIME_ENV_KEYS = ["APPIMAGE", "APPDIR", "ARGV0", "OWD"] as const;
1072// PATH-style variables the AppImage runtime prepends with its temporary mount
1073// (e.g. /tmp/.mount_T3-XXXX/usr/bin). Only the mount segments are dropped; the
1074// user's real entries are preserved.
1075const APPIMAGE_PATH_LIKE_ENV_KEYS = ["PATH", "LD_LIBRARY_PATH"] as const;
1076
1077function isPathSegmentUnderAppDir(segment: string, appDir: string): boolean {
1078 return segment === appDir || segment.startsWith(`${appDir}/`);
1079}
1080
1081// On Linux AppImage builds the runtime mounts the app under a temporary dir and
1082// injects APPIMAGE/APPDIR/ARGV0/OWD plus mount entries on PATH/LD_LIBRARY_PATH.
1083// The integrated terminal inherits the server process environment, so without
1084// this scrub those leak into the PTY and tools resolve against the AppImage
1085// mount instead of the user's real environment (e.g. `php` reporting
1086// PHP_BINARY as the AppImage path). See issue #1699. The scrub is gated on an
1087// actual AppImage launch so non-AppImage environments are left untouched.
1088function stripAppImageRuntimeEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
1089 if (env.APPIMAGE === undefined && env.APPDIR === undefined) return env;
1090
1091 const scrubbed: NodeJS.ProcessEnv = { ...env };
1092 for (const key of APPIMAGE_RUNTIME_ENV_KEYS) {
1093 delete scrubbed[key];
1094 }
1095
1096 const appDir = env.APPDIR?.replace(/\/+$/, "");
1097 if (appDir) {
1098 for (const key of APPIMAGE_PATH_LIKE_ENV_KEYS) {
1099 const value = scrubbed[key];
1100 if (value === undefined) continue;
1101 const kept = value
1102 .split(":")
1103 .filter((segment) => segment.length > 0 && !isPathSegmentUnderAppDir(segment, appDir));
1104 if (kept.length > 0) {
1105 scrubbed[key] = kept.join(":");
1106 } else {
1107 delete scrubbed[key];
1108 }
1109 }
1110 }
1111
1112 return scrubbed;
1113}
1114
10681115function createTerminalSpawnEnv(
10691116 baseEnv: NodeJS.ProcessEnv,
10701117 runtimeEnv?: Record<string, string> | null,
......@@ -1080,7 +1127,7 @@ function createTerminalSpawnEnv(
10801127 spawnEnv[key] = value;
10811128 }
10821129 }
1083 return spawnEnv;
1130 return stripAppImageRuntimeEnv(spawnEnv);
10841131}
10851132
10861133function normalizedRuntimeEnv(