| author | |
| committer | |
| log | 1047dac0c5296b1c548e607f061f95227a706b49 |
| tree | 89dc8b957857cdc21683554a69b24f9ae585d81a |
| parent | 143b9c9280015f0ff9fa70cd9e4edb18e975c94d |
| signature | Signed by PGP key B5690EEEBB952194 |
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( |
| 1334 | 1334 | }), |
| 1335 | 1335 | ); |
| 1336 | 1336 | |
| 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 | ||
| 1337 | 1394 | it.effect("injects runtime env overrides into spawned terminals", () => |
| 1338 | 1395 | Effect.gen(function* () { |
| 1339 | 1396 | const { manager, ptyAdapter } = yield* createManager(); |
apps/server/src/terminal/Manager.ts+48-1| ... | ... | @@ -1065,6 +1065,53 @@ function shouldExcludeTerminalEnvKey(key: string): boolean { |
| 1065 | 1065 | return TERMINAL_ENV_BLOCKLIST.has(normalizedKey); |
| 1066 | 1066 | } |
| 1067 | 1067 | |
| 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. | |
| 1071 | const 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. | |
| 1075 | const APPIMAGE_PATH_LIKE_ENV_KEYS = ["PATH", "LD_LIBRARY_PATH"] as const; | |
| 1076 | ||
| 1077 | function 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. | |
| 1088 | function 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 | ||
| 1068 | 1115 | function createTerminalSpawnEnv( |
| 1069 | 1116 | baseEnv: NodeJS.ProcessEnv, |
| 1070 | 1117 | runtimeEnv?: Record<string, string> | null, |
| ... | ... | @@ -1080,7 +1127,7 @@ function createTerminalSpawnEnv( |
| 1080 | 1127 | spawnEnv[key] = value; |
| 1081 | 1128 | } |
| 1082 | 1129 | } |
| 1083 | return spawnEnv; | |
| 1130 | return stripAppImageRuntimeEnv(spawnEnv); | |
| 1084 | 1131 | } |
| 1085 | 1132 | |
| 1086 | 1133 | function normalizedRuntimeEnv( |