dotnet / dotnet/vscode-dotnet-runtime
XDG_RUNTIME_DIR inconsistency can break cross-process mutex isolation
- Dominant language
- TypeScript
- Stars
- 209
- Forks
- 455
- PR merge metrics
- No merged PRs in 30d
Description
## Summary
The `NodeIPCMutex` IPC socket path depends on `XDG_RUNTIME_DIR` on Linux. When this variable is inconsistently set across VS Code processes (e.g. one SSH session has it, another doesn't), the mutex silently fails to provide cross-process exclusion because each process creates sockets in different directories.
## Background
In `NodeIPCMutex.getIPCHandlePath`:
```typescript
const ipcPathDir = os.platform() === 'win32' ? `\\\\.\\pipe\\` :
os.platform() === 'linux' && process.env.XDG_RUNTIME_DIR ? process.env.XDG_RUNTIME_DIR : os.tmpdir();
```
- When `XDG_RUNTIME_DIR` is set (typical): sockets go to `/run/user//vscd-*.sock`
- When unset: sockets go to `/tmp/vscd-*.sock`
## What is `XDG_RUNTIME_DIR`?
`XDG_RUNTIME_DIR` is defined by the [XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/latest/) as a per-user directory for runtime files (sockets, lock files, etc.). On modern Linux systems with systemd, it is set to `/run/user//` by `pam_systemd` (a PAM module invoked by `logind`) when a user login session is created.
Key properties of `/run/user//`:
- **tmpfs-backed** (RAM filesystem), wiped on logout/reboot
- **User-owned**, mode `0700` — only the owning user can read/write
- Created automatically by `pam_systemd` during login
- Used by many system tools: systemd, PulseAudio, D-Bus, Wayland, GNOME/KDE, Pipewire
## When is `XDG_RUNTIME_DIR` not set?
`pam_systemd` must be in the PAM stack for `XDG_RUNTIME_DIR` to be set. It is **absent** in:
- **SSH without PAM**: `UsePAM no` in `sshd_config` — no PAM modules run, no `pam_systemd`, no `XDG_RUNTIME_DIR`
- **`su` / `sudo -u`**: Switching users without a full login session does not invoke `pam_systemd`
- **Expired login sessions**: systemd may clean up `/run/user//` when it considers the login session ended, even if SSH connections persist (depends on `KillUserProcesses` and `lingering` settings in `logind.conf`)
VS Code Remote SSH inherits whatever the shell session provides. If the SSH connection doesn't go through `pam_systemd`, the variable won't be set.
## Concurrency Problem
If two VS Code instances connect to the same remote machine but one has `XDG_RUNTIME_DIR` set and the other doesn't:
- **Instance A** (with `XDG_RUNTIME_DIR`): creates `/run/user/1000/vscd-installedLk.sock`
- **Instance B** (without): creates `/tmp/vscd-installedLk.sock`
These are **different locks**. Both instances believe they hold exclusive access to the extension state, but they don't. This can lead to:
- Corrupted install records in `globalState`
- Race conditions during install/uninstall operations
- Phantom installs that appear in one session but not another
## Possible Mitigations
1. **Always use `os.tmpdir()`** — simplest, but changes behavior for all existing Linux users and `/tmp/` may have stricter permissions or be cleaned more aggressively.
2. **Use a path derived from the VS Code `globalStoragePath`** — guaranteed consistent per-extension across all processes, but changes the socket location for everyone.
3. **Prefer `XDG_RUNTIME_DIR` but fall back to a well-known user-specific directory** (e.g. `~/.cache/vscode-dotnet/`) — avoids `/tmp/` permission issues while staying consistent.
4. **Log a warning when `XDG_RUNTIME_DIR` is unset on Linux** — at minimum, make the inconsistency visible in diagnostics.
## Additional Context
The `EACCES` issue may also be related — if `/run/user//` is cleaned up by systemd mid-session (e.g. the login session expired while the SSH connection stayed alive), the socket directory may become inaccessible, triggering `EACCES` on the next `server.listen` call.
https://github.com/dotnet/vscode-dotnet-runtime/issues/2658
Contributor guide
Assessment
This issue has not been assessed yet.