Skip to content

Commit ca28251

Browse files
fix(launcher-standard): move PID/log to XDG dirs (security: symlink-attack hardening) (#175)
## Summary The launcher-standard specified `/tmp/{app-name}-server.pid` and `/tmp/{app-name}-server.log` as the required runtime paths. Predictable names in a world-writable directory are a **symlink-attack target** on any shared host: - An attacker can pre-create `/tmp/<app>-server.pid` containing their own PID. The launcher's `is_running()` returns true, and `stop_server()` will `kill <attacker-pid>` — DoS or signal-handling abuse. - Similar pattern for the log: pre-symlink `/tmp/<app>-server.log` → some target the attacker wants clobbered, then the launcher's `nohup ... > LOG_FILE` does the write. - The standard already warns "don't log sensitive information" (§Best Practices > Security), but the predictable-path defence is belt-and-braces. ## Fix Route both to XDG dirs with documented fallback ladders: - **PID** → `${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}/<app>-server.pid`. `$XDG_RUNTIME_DIR` is mode `0700` and user-scoped per the XDG Base Directory spec (Linux). `$TMPDIR` covers macOS / BSDs (typically `/var/folders/.../T`, per-user). `/tmp` remains only as a last-resort fallback for hosts that set neither (rare). - **Log** → `${XDG_STATE_HOME:-$HOME/.local/state}/<app>/server.log`. Per-user, survives reboot, not world-writable. The `<app>` subdir isolates each launcher's logs. ## Changes Both files in the same commit per the lock-step requirement (and the gate in #172): - **`launcher/launcher-standard.a2ml`** - `[runtime].pid-file-pattern` / `log-file-pattern` updated with fallback ladders + commented rationale - `[disinteg].preserve` updated to reference the new log dir - **`docs/UX-standards/launcher-standard.adoc`** - Standard Launcher Template snippet uses XDG paths + `mkdir -p` for the state dir - §What `--disinteg` removes / does not remove: paths updated - Desktop File `Exec=` example log-arg updated - Calling Convention daemon-chain example updated - Debugging Checklist now uses `$LOG_FILE` / `$PID_FILE` variable refs rather than literal paths - §Best Practices > Logging: lead bullet rewritten with rationale - §Best Practices > Security: new lead bullets explaining the attack vector and the XDG choice - §Compliance Checklist: "Log to predictable location (`/tmp/...`)" replaced with the XDG requirement Remaining `/tmp/` mentions in the prose are in *forbidden-patterns* text that explicitly tells readers NOT to use `/tmp` — intentional. ## Compatibility Bash-expansion syntax (`${VAR:-${VAR2:-/literal}}`) matches the shell-expansion style already in the a2ml (e.g. `$HOME/.local/share/applications` in `[integration.linux]`). Any consumer that already interpolates `$HOME` here will handle `${...:-...}` without changes. Existing launchers that hard-coded `/tmp/<app>-server.pid` continue to work but become non-compliant; no break for them, just a green-field contract change for new launchers and a flagged migration for old ones. ## Coordination - Independent of #170, #171, #172, #173. No file overlap. - Lock-step gate (#172) will go green on first push (both files in diff). ## Notes - Spec `[spec].version` intentionally NOT bumped — five PRs (#170, #171, #173, this one, plus any future) all touch the contract; the merger sequences them. ## Test plan - [x] a2ml parses (python tomllib): new patterns round-trip cleanly, `[disinteg].preserve` reflects the new log path - [x] No `/tmp/` literals remain in non-forbidden-patterns prose (grep verified) - [ ] Live lock-step gate (#172) goes green on first push 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a12f765 commit ca28251

2 files changed

Lines changed: 33 additions & 15 deletions

File tree

docs/UX-standards/launcher-standard.adoc

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,16 @@ The reference implementation is provided in link:comprehensive-launcher-template
2020
----
2121
# Standardized structure for all launchers
2222
APP_NAME="MyApp" # Application name
23-
REPO_DIR="/path/to/repo" # Repository directory
23+
REPO_DIR="/path/to/repo" # Repository directory
2424
COMMAND="command to run" # Startup command
2525
URL="http://localhost:PORT" # Web URL (if applicable)
26-
PID_FILE="/tmp/app-server.pid" # Process ID tracking
27-
LOG_FILE="/tmp/app-server.log" # Log file location
26+
# PID file in XDG_RUNTIME_DIR (mode 0700, user-scoped) — falls back to
27+
# $TMPDIR (macOS) then /tmp (last resort). See §Best Practices > Security.
28+
PID_FILE="${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}/${APP_NAME}-server.pid"
29+
# Log file in XDG_STATE_HOME (defaults to $HOME/.local/state per spec).
30+
LOG_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/${APP_NAME}"
31+
LOG_FILE="${LOG_DIR}/server.log"
32+
mkdir -p "$LOG_DIR"
2833
MODE="${1:---auto}" # Default mode
2934
----
3035

@@ -289,13 +294,13 @@ system". The `--disinteg` mode is its exact inverse.
289294

290295
Everything `--integ` created, plus:
291296

292-
* The PID file (`/tmp/<app>-server.pid`)
297+
* The PID file (`$XDG_RUNTIME_DIR/<app>-server.pid`, or the resolved equivalent — see §Best Practices > Security)
293298
* Any `.bat` fallback shortcuts written when PowerShell wasn't available
294299

295300
It deliberately **does not** remove:
296301

297302
* `~/.config/<app>/` — user preferences survive reinstall
298-
* `/tmp/<app>-server.log` — logs stay for post-mortem
303+
* `$XDG_STATE_HOME/<app>/` (defaults to `$HOME/.local/state/<app>/`) — logs stay for post-mortem
299304
* The source repository at `REPO_DIR`
300305

301306
The removal instructions for those are printed after `--disinteg` so the user
@@ -324,7 +329,7 @@ feedback.
324329
[Desktop Entry]
325330
Type=Application
326331
Name=Application Name
327-
Exec=/var/mnt/eclipse/repos/.desktop-tools/keepopen.sh "AppName" "/path/to/repo" "GUI_CMD" "TUI_CMD" "/tmp/app.log"
332+
Exec=/var/mnt/eclipse/repos/.desktop-tools/keepopen.sh "AppName" "/path/to/repo" "GUI_CMD" "TUI_CMD" "$HOME/.local/state/app/server.log"
328333
Terminal=true # keepopen needs a terminal for its loud banners and shell fallback
329334
Icon=/path/to/icon.png
330335
Categories=Category;
@@ -398,7 +403,7 @@ a `tail -f LOG` after the launcher so the terminal stays open:
398403

399404
[source,bash]
400405
----
401-
"aerie-launcher.sh --auto && tail -f /tmp/aerie.log"
406+
"aerie-launcher.sh --auto && tail -f $HOME/.local/state/aerie/server.log"
402407
----
403408

404409
=== The three stages
@@ -486,10 +491,10 @@ Exec=launcher.sh --auto
486491

487492
=== Debugging Checklist
488493

489-
1. Check log file: `tail -f /tmp/app-server.log`
494+
1. Check log file: `tail -f "$LOG_FILE"` (resolves to `$XDG_STATE_HOME/<app>/server.log`)
490495
2. Verify process: `ps aux | grep app-name`
491496
3. Test URL manually: `curl -v http://localhost:PORT`
492-
4. Check PID file: `cat /tmp/app-server.pid`
497+
4. Check PID file: `cat "$PID_FILE"` (resolves to `$XDG_RUNTIME_DIR/<app>-server.pid`)
493498
5. Test browser opening: `xdg-open http://localhost:PORT`
494499
6. Verify dependencies: `command -v required-command`
495500
7. Check port availability: `ss -tlnp | grep PORT`
@@ -537,7 +542,7 @@ APP_NAME="AppName"
537542
[ ] Implement `wait_for_server()` with reasonable timeout
538543
[ ] Add proper error handling and user feedback
539544
[ ] Provide clear success/failure messages
540-
[ ] Log to predictable location (`/tmp/app-name.log`)
545+
[ ] Log to XDG state dir (`$XDG_STATE_HOME/<app>/server.log`, defaults to `$HOME/.local/state/<app>/server.log`); never use `/tmp/<app>.log`
541546
[ ] Handle browser launch failures gracefully
542547
[ ] Provide manual fallback instructions
543548
[ ] Implement `--start`, `--stop`, `--status` modes
@@ -548,7 +553,8 @@ APP_NAME="AppName"
548553
== Best Practices
549554

550555
=== Logging
551-
- Log to `/tmp/app-name.log` for easy debugging
556+
- Log to `$XDG_STATE_HOME/<app>/server.log` (defaults to `$HOME/.local/state/<app>/server.log` per XDG spec). Per-user, survives reboot, not world-writable.
557+
- Never log to `/tmp/<app>.log`. Predictable names in a world-writable dir are a symlink-attack target on shared hosts — see §Best Practices > Security.
552558
- Include timestamps for long-running processes
553559
- Rotate logs if they grow large
554560
- Provide log location in error messages
@@ -567,7 +573,9 @@ APP_NAME="AppName"
567573
- Optimize startup sequence
568574

569575
=== Security
570-
- Use predictable but unique PID file names
576+
- **PID files MUST go in `$XDG_RUNTIME_DIR`** (Linux) / `$TMPDIR` (macOS), not `/tmp`. `$XDG_RUNTIME_DIR` is mode `0700` and user-scoped per the XDG Base Directory spec; `$TMPDIR` on macOS is `/var/folders/.../T` (per-user). `/tmp` is world-writable: an attacker on a shared host can pre-create `/tmp/<app>-server.pid` containing their own PID, after which the launcher's `is_running()` returns true and `stop_server()` will `kill <attacker-pid>` — DoS or signal-handling abuse vector. The fallback ladder `${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}` exists only as a last resort for hosts that set neither (rare).
577+
- **Log files MUST go in `$XDG_STATE_HOME`** for the same reason — never `/tmp/<app>.log`.
578+
- Use predictable but unique PID file *names within the chosen dir* (not in `/tmp`).
571579
- Clean up PID files on exit
572580
- Don't log sensitive information
573581
- Validate URLs before opening

launcher/launcher-standard.a2ml

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,18 @@ mode = "--auto"
7272
[runtime]
7373
# Required runtime behaviour.
7474
background = "nohup"
75-
pid-file-pattern = "/tmp/{app-name}-server.pid"
76-
log-file-pattern = "/tmp/{app-name}-server.log"
75+
# PID files live in the user's runtime-state dir — wiped on logout,
76+
# user-scoped (mode 0700 per XDG spec), no symlink-attack target.
77+
# Bash-expansion ladder:
78+
# 1. $XDG_RUNTIME_DIR — Linux per freedesktop XDG Base Directory spec
79+
# 2. $TMPDIR — macOS / BSDs (typically /var/folders/.../T, per-user)
80+
# 3. /tmp — last resort (predictable, world-writable; flagged
81+
# in §Best Practices > Security)
82+
pid-file-pattern = "${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}/{app-name}-server.pid"
83+
# Logs go to XDG_STATE_HOME (per spec, defaults to $HOME/.local/state).
84+
# Per-user, survives reboot, not world-writable. The {app-name} subdir
85+
# isolates each launcher's logs.
86+
log-file-pattern = "${XDG_STATE_HOME:-$HOME/.local/state}/{app-name}/server.log"
7787
wait-for-url-timeout-seconds = 15
7888
startup-command-search = [
7989
"{repo-dir}/scripts/run.sh",
@@ -134,7 +144,7 @@ remove = [
134144
]
135145
preserve = [
136146
"$HOME/.config/{app-name}/",
137-
"/tmp/{app-name}-server.log",
147+
"${XDG_STATE_HOME:-$HOME/.local/state}/{app-name}/",
138148
"{repo-dir}",
139149
]
140150

0 commit comments

Comments
 (0)