Skip to content

A root dotfiles step poisons the shared pixi cache: #240's second clause, by a route it did not consider #661

Description

@blooop

#240 closed with the mount moved out of $HOME, which fixed the root-owned ~/.cache. Its "Done when" carried a second clause that was not done:

Decide and record what happens when the container uid cannot write the cache — at minimum it should not be a hard provisioning failure.

On 19 Sep 2026 that clause came due. Three workspaces came up with no nvim, no git-delta, no jq, no forgit, no claude-statusline and no xclip. Every launch exited 0 and printed Dotfiles setup completed successfully!. The symptom the user saw was nvim: command not found.

It arrived by a route #240 did not consider. #240 reasoned about an image whose user is not uid 1000, and concluded the common case is safe because every mainstream base declares uid 1000 and this host is uid 1000. That holds. But the container uid is not the only uid that writes this cache — the dotfiles step picks its own user, and it falls back to root.

What is in the cache now

One file, written by uid 0 into the directory every other container reads:

-rw------- 1 root root 5051 Sep 19 10:46 repodata/shards-v1/125d7bea…msgpack
4964 4951 259:2 /home/ags/.cache/devlaunch/pixi /var/tmp/devlaunch-pixi rw,relatime - ext4 /dev/nvme0n1p2 rw

#240 guessed the residual hazard as package subdirectories at 0755, and expected 0777 on the host directory to rescue the cross-uid case. Neither holds for repodata. Every shard in repodata/shards-v1 is 0600 — all 1156 of them, including the shards the uid-1000 containers wrote themselves. That is the mode pixi writes, not a umask artifact, so no mode on the parent and no umask in the container changes it. A shard written by another uid is unreadable, full stop.

Every pixi global sync afterwards died on that one file:

 WARN Couldn't sync environment neovim
  × failed to read from file `/var/tmp/devlaunch-pixi/repodata/shards-
  │ v1/125d7bea…msgpack`: Permission denied (os error 13)

Error:   × Couldn't sync the following environments: git-delta, forgit, jq,
  │ claude-statusline, neovim, xclip

Repodata rather than a package, so it took out every environment whose solve needed that shard instead of one of them. The env directories were all created, which is why nothing looked absent — they were never populated and never exposed.

Where the root came from

devpod's dotfiles step picks its user by reading the ssh alias back out of the config, and falls through to root when it finds none — pkg/dotfiles/dotfiles.go:139, devpod 0.26.1:

remoteUser, err := devssh.GetUser(
    p.client.WorkspaceConfig().ID,
    p.client.WorkspaceConfig().SSHConfigPath,
    p.client.WorkspaceConfig().SSHConfigIncludePath,
)
if err != nil {
    remoteUser = "root"
}

Both WorkspaceConfig() fields are always empty. up writes neither, and a workspace.json here carries only id, uid, provider, machine, ide, source, creationTimestamp, lastUsed and context. So GetUser always reads ~/.ssh/config, and the rule reduces to one sentence: the dotfiles step runs as root whenever ~/.ssh/config holds no alias for the workspace at that moment.

In this instance the alias was missing because I had moved the DevPod blocks to ~/.ssh/config.d/devpod while testing devpod's SSH_CONFIG_INCLUDE_PATH option. That is reverted, and it is not the trigger that matters. The trigger that matters is that a missing alias is an open, unreproduced bug in its own right — blooop/dotfiles#40, where a machine lost its host alias and spent 15 hours attaching as root. Every path to a missing alias is now also a path to a poisoned cache for every workspace launched afterwards.

Why this is expensive to find

  • It outlives the launch that caused it. The poisoned file survives the container that wrote it, so the workspace that breaks the cache is not the workspace that reports it. Mine was written 30 minutes before the first affected launch.
  • Nothing fails. devpod up exits 0 and reports the dotfiles setup as successful while the sync underneath reports six unsynced environments. This is Shared pixi cache mount makes ~/.cache root-owned in any image that lacks it, and hard-fails on a uid mismatch #240's unfinished clause exactly: a hard failure of provisioning, presented as a success.

Timeline

All UTC, one host:

  • 09:46:03 — the root dotfiles step. /root/.pixi in that container, and the root:root shard in the shared cache.
  • 10:15:52, 10:15:56, 10:36:44 — three unrelated workspaces run their dotfiles step. All three fail the same way. All three report success.

Repro

  1. Take a workspace whose alias is not in ~/.ssh/config. Delete the block, or set the SSH_CONFIG_PATH context option and let up write elsewhere — that reaches the same place through the never-written WorkspaceConfig().SSHConfigPath.
  2. devpod up <ws>. Confirm the dotfiles step went root: /root/.pixi exists in the container, and the log says Added /root/.pixi/bin to PATH.
  3. find ~/.cache/devlaunch/pixi ! -user $USER — expect a fresh 0600 shard under repodata/shards-v1.
  4. Create any other workspace. Its pixi global sync fails on that shard, and its shell arrives with no nvim.

Candidate fixes

  • Key the cache by uid — PIXI_CACHE_TARGET plus id -u, or a per-uid host source. This closes the class rather than this instance, and it does not wait on devpod fixing its root fallback. A root dotfiles step then costs its own container a download and nothing else.
  • Withhold the cache from a non-matching uid. Cheaper, same containment, and it makes a root step slow instead of contagious.
  • Surface an unsynced environment. Independent of the cache, and the reason this cost three workspaces and a bisect rather than one line of output. This is the clause Shared pixi cache mount makes ~/.cache root-owned in any image that lacks it, and hard-fails on a uid mismatch #240 left open.

I would take the uid keying and the reporting together: the first stops it spreading, the second stops it hiding.

Meanwhile

Check: find ~/.cache/devlaunch/pixi ! -readable -type f. One hit breaks every workspace.

Repair: delete the offending file, then re-run pixi global sync in each workspace. repodata/shards-v1 and pkgs belong to the host user, so removing a root-owned file inside them needs no sudo. Removing a root-owned directory does.

Measured on dl 0.53.0 / devpod 0.26.1. PIXI_CACHE_TARGET is unchanged on dl 0.57.1.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions