anthropics / anthropics/claude-code

Sockets directory is still first-come after 2.1.248, and a local account can squat both /tmp/cc-socks and the per-uid fallback

Abierto
#91,223 0 comentarios 0 reacciones 0 asignados Ver en GitHub
area:core area:security bug has repro platform:macos
Lenguaje dominante
Python
Estrellas
145k
Forks
23.1k
Métricas de merge de PR
Métricas de PR pendientes

Descripción

## Summary

Version 2.1.248 fixed the symptom reported in #89401. A second account on a shared host now keeps cross-session messaging, because it falls back to `/tmp/cc-socks-`. Two problems remain.

1. The sockets directory is still first-come. `/tmp/cc-socks` belongs to whichever account starts Claude Code first after each `/tmp` clear. The directory an account uses therefore changes from boot to boot.
2. Any unprivileged local account can take both names. The fallback has no fallback, so messaging goes off again.

This report also documents a workaround, because the current one in the error text does not survive case 2.

## Environment

- Claude Code 2.1.252, native install, macOS 25.6.0 (arm64)
- Two local accounts, uid 501 and uid 502
- `XDG_RUNTIME_DIR` unset, `CLAUDE_CODE_TMPDIR` unset

## 1. First-come ownership

The primary path keeps the shared name:

```js
var Y = 103;
function Yur(){
let e = a.XDG_RUNTIME_DIR || ly(),
t = ue(F(e, "cc-socks", `${process.pid}.sock`));
if (Buffer.byteLength(t) <= Y) return t;
return nNn();
}
```

`/tmp` is world-writable and sticky. The first account to start Claude Code creates `/tmp/cc-socks` at mode 0700. That account keeps it. Every other account fails the owner check and takes the per-uid path. macOS clears `/tmp`, so the winner can change after a reboot.

Peer discovery does not break. Each session writes its absolute socket path into `~/.claude/sessions/.json`:

```json
"pid": "45417",
"messagingSocketPath": "/tmp/claude-501/cc-socks/45417.sock",
"peerProtocol": "1"
```

Peers read that file, so they never resolve the directory by convention. The cost is diagnostic, not functional. An account cannot predict its own socket directory, and support answers now depend on login order.

The uid suffix costs 4 bytes out of the 103-byte budget. Make `cc-socks-` the default. This removes the shared name, the boot-order dependency, and the first squat target below.

## 2. A local account can squat both names

The refusal path tries the per-uid directory once, then stops:

```js
try { await Re(o) }
catch (m) {
let k = un(m) ? await XGe() : void 0,
u = k === void 0 ? void 0 : ue(nNn(k)),
_ = u === void 0 ? void 0 : F(u, "..");
if (u === void 0 || _ === void 0 || _ === o) return $e(o, m, De(m));
n(`[uds-messaging] sockets directory ${o} refused (...); trying the per-uid fallback ${_}`, {level:"warn"});
try { await Re(_) } catch (f) { return $e(_, f, De(f)) }
e = u; we("primary_dir_refused_fell_back")
}
```

`nNn()` builds the fallback from a fixed uid, so the target name is public and predictable:

```js
function nNn(e = process.getuid?.() ?? 0){
let t = a.TERMUX_VERSION ? a.PREFIX : void 0,
i = t ? F(t, "tmp") : "/tmp";
return F(i, `cc-socks-${e}`, `${process.pid}.sock`);
}
```

### Steps to reproduce

1. As any local account, and before the target account starts Claude Code, run these two commands. Replace 501 with the target uid.

```
mkdir -m 700 -p /tmp/cc-socks
mkdir -m 700 -p /tmp/cc-socks-501
```

2. As uid 501, start Claude Code.
3. Read the debug log.

### Result

The primary directory fails the owner check. The per-uid directory fails the same check. The code calls `$e()` and cross-session messaging turns off for that account.

Both directories survive a `/tmp` clear if the other account recreates them from a login item or a launchd agent.

### Scope

This is denial of service only. The owner check and the `peerToken` file still stop the squatting account from receiving anyone else's messages. The attacker needs a local login on the same host. The realistic targets are shared Macs, build hosts, and CI runners with several service accounts.

### Suggested fix

Try a third location before giving up. A directory under the user's own home works, because another account cannot create it. Report the refusal in `/status` in both cases.

## 3. Workaround

Set `XDG_RUNTIME_DIR` to a private directory that you own:

```
XDG_RUNTIME_DIR=/tmp/claude-501
```

Claude Code already creates `/tmp/claude-` at mode 0700 for its own temp files, and validates it. The directory exists and passes the checks. The resulting socket path is 35 bytes, which is well inside the 103-byte limit.

This also defeats the squat in case 2, because the primary path no longer resolves to `/tmp/cc-socks`.

Two notes on the workaround:

- Set it in your shell profile or in user settings. Version 2.1.251 stopped project-level `.claude/settings.json` from setting `CLAUDE_CODE_TMPDIR`, `TMPDIR`, `TMP`, and `TEMP`.
- The fallback ignores both `XDG_RUNTIME_DIR` and `CLAUDE_CODE_TMPDIR`, as `nNn()` above shows. If your own directory is ever refused, the code sends you to `/tmp` anyway. The error text asks the user to point these variables at a private directory. The fallback should honor the same base.

Guía de contribución

No hay ninguna guía de contribución indexada para este repositorio

Línea de trabajo

Start by locating Yur(), nNn(), and the refusal path shown in the issue, then trace how XDG_RUNTIME_DIR and CLAUDE_CODE_TMPDIR are resolved. Reproduce the two-account commands and verify that socket selection no longer depends on first-come /tmp names, a private fallback is attempted before failure, and /status reports both refusals.

Escrito por el modelo de indexación a partir del texto del issue.

Evaluación

Stack tecnológico
javascript, macos
Área
cli, operating-systems, security
Tipo de issue
Error
Dificultad
4/5
Tiempo estimado
3-5 días
Estado de actividad
Activo
Claridad
Bastante claro
Aptitud para principiantes
52/100

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.