Skip to content

Shared pixi cache mount makes ~/.cache root-owned in any image that lacks it, and hard-fails on a uid mismatch #240

Closed

Description

@blooop

The shared pixi cache mount (#232, PR #236) targets /home/vscode/.cache/devlaunch-pixi. When the image has no /home/vscode/.cache — which the stock devcontainer images do not — Docker creates the missing parent as root:root, and the container user loses ~/.cache entirely.

Measured (2026-08-16, docker 29.7.2)

Stock mcr.microsoft.com/devcontainers/base:ubuntu, run as uid 1000 (vscode):

~/.cache owner mkdir ~/.cache/pip-test
no mount (control) 1000 OK
with the mount as shipped 0 (root) Permission denied

The pixi mount itself stays writable, so pixi global sync is fine — everything else that writes ~/.cache (pip, uv, pre-commit, fontconfig…) silently breaks.

Which images are hit — it depends only on whether the image already ships ~/.cache:

image ships ~/.cache with mount
devcontainers/base:ubuntu, base:ubuntu-24.04, rust:latest no BROKEN
wayfinder-devcontainer (built), devlaunch-main (built) yes (uid 1000) writable

So our own built images are unaffected, and every stock third-party devcontainer is. Mount args are appended unconditionally (dl.py:3656), so no dotfiles are involved; it applies to newly created containers only, since mounts are fixed at create time.

Second, related failure: cross-uid writes

PIXI_CACHE_DIR pointing at a directory the container user cannot write is a hard failure, not a degradation — pixi does not fall back to reading. Measured with a warm cache already containing the package:

× failed to open '/shared/repodata/....shards-cache-v1'
╰─▶ Permission denied (os error 13)
Error: × Couldn't install the following environments: jq     (exit 1)

Control: the same uid with its own writable cache installs fine, so the cause is the cache's ownership, not the uid. Sharing therefore requires container uid == host uid, or root. Every mainstream base declares remoteUser = vscode/node at uid 1000, and this host is uid 1000, so the common case is safe; an image with any other uid fails to provision tools at all.

Making the host dir 0777 does rescue the cross-uid case in a simple test, but it is not a rigorous fix: package subdirectories are created 0755 owned by whichever uid wrote them, so two uids can still block each other on a shared package or repodata file.

Third: the dotfiles chown amplifier

blooop/dotfiles install.sh:44-48 chowns -R "$USER:$USER" "$HOME/.cache" when it finds it root-owned — and the mount manufactures exactly that precondition. Verified write-through: a container at uid 2000 chowned the host directory 1000 → 2000, after which the host user could not write its own cache. A second container at uid 1000 chowns it back — the two ping-pong on every launch. (install.sh:131-133 reaches it by a second path, guarded on .config writability but chowning .cache too.)

Done when

The mount target moves out of $HOME — e.g. /var/cache/devlaunch/pixi, whose parent exists in every image — which fixes all three at once: no root-owned ~/.cache, no dependence on the container's home path or username, and out of reach of the dotfiles chown. Pin it with a test asserting the target is not under any home directory, and add a preflight exists() check on the mount source (a typo'd source currently fails up loudly, then a retried ssh silently starts a container without the mount). Decide and record what happens when the container uid cannot write the cache — at minimum it should not be a hard provisioning failure.

This ticket was generated by AI during investigation follow-through; all measurements above were taken live on 2026-08-16.

Activity

  1. self-assigned this
    on Aug 16, 2026
  2. blooop commented on Aug 16, 2026

    @blooop
    OwnerAuthor

    breadcrumb: Building this as: move the shared pixi cache mount target from /home/vscode/.cache/devlaunch-pixi to /var/cache/devlaunch/pixi, so the mount can no longer manufacture a root-owned ~/.cache in an image that ships none.

    Seams I will test at, all pre-agreed by the ticket:

    • the up argv builder for the pixi cache — the existing shared-pixi-cache suite, which already pins mount/workspace-env/dotfiles-script-env spellings. New test there asserting the invariant the regression violated: the target is under no home directory (/home/..., /root/...). Red first.
    • the whole-argv cold-launch pin in the spawn-count suite, which pins the up command entire and so has to move in step.
    • the preflight: the mount args are emitted only when the source really exists, so a source that vanished between creation and use produces no half-mounted container.

    Verified on the real image before writing code: /var/cache exists in stock devcontainers/base:ubuntu (root:root, FHS-standard), the mounted dir is writable by uid 1000, and ~/.cache stays user-owned — where the shipped target leaves it root-owned and unwritable, reproducing the ticket's table exactly.

  3. blooop commented on Aug 16, 2026

    @blooop
    OwnerAuthor

    This was generated by AI during investigation follow-through.

    Lifecycle behaviour, measured live (devpod 0.26.1 / docker 29.7.2, isolated DEVPOD_HOME). These decide what the fix has to document about existing workspaces.

    operation mount present afterwards container
    up with the mount yes new
    stop then up (mount not re-passed) yes — survives same id
    up --recreate without re-passing --mount no — lost new id
    up --recreate with --mount yes new id
    up --mount … on an existing container no — silent no-op (exit 0, no warning, no docker run) unchanged

    Two consequences:

    1. devpod does not persist --mount into workspace config. It must be re-passed on every up, which devlaunch does — but it also means a mount can only ever be added by a recreate. A devlaunch upgrade therefore does not reach existing workspaces: they keep running without the cache, silently, until dl <ws> recreate.
    2. The ~/.cache breakage heals on recreate but not on stop/start. /home/vscode sits on the container's overlay layer, so stop+up keeps the root-owned directory, while --recreate gets a fresh layer where .cache is recreated vscode-owned. So the remedy for anyone already affected is dl <ws> recreate, and that is worth a README line.

    Also confirmed about the mechanism itself: the root-owned ~/.cache arises from any root-context command that touches $HOME before the user does — the mount is the reliable trigger, not the only one.

    Caveat, untested: a devcontainer that mounts the user's home as a named volume would not heal on recreate, because home would no longer be on the disposable layer. Worth one follow-up check before promising the remedy unconditionally.

  4. blooop commented on Aug 16, 2026

    @blooop
    OwnerAuthor

    breadcrumb: PR #241, green.

    The mount target is now /var/cache/devlaunch/pixi — FHS-required parent, present in every base this launches, outside every home. Verified on the real stock image before writing code: with the shipped target ~/.cache comes out root-owned and unwritable; with the new one it stays the user's own and the mounted dir is writable. Moving out of $HOME also drops the hardcoded remote-user guess and puts the cache out of reach of dotfiles installs that chown a root-owned home cache through the bind onto the host.

    Pinned red-first by the invariant the regression violated — the target is under no home directory, stated over homes rather than over the one path that regressed — plus a parent-exists assertion and a preflight that emits the mount args only when the source really exists. Existing argv and whole-argv spawn pins moved in step.

    The uid question is recorded as a documented constraint rather than a fix: pixi hard-fails on a cache it cannot write instead of degrading to reads, and devlaunch cannot see a container's uid before launching it, so there is nothing to decide from the outside. The alternatives considered and rejected are written up in the PR body rather than built. Common case is safe; README states the limit and the recovery.

    One thing a reviewer should weigh: this regression is on main only — the feature that introduced it has not been released — so the CHANGELOG carries both a Fixed entry and a corrected path in the original Added entry, which sit in the same Unreleased block.

  5. blooop commented on Aug 16, 2026

    @blooop
    OwnerAuthor

    This was generated by AI during review.

    Review of PR #241: Standards Approve, Spec Request changes — and the blocking finding was real, reproduced independently before acting.

    Moving the target to /var/cache/devlaunch/pixi fixed the ~/.cache regression but broke the migration seam: devpod re-applies --workspace-env on every up while mounts only land at creation, so every pre-existing workspace would get PIXI_CACHE_DIR pointing at an unmounted, root-owned path — pixi global install exit 1, where today's target merely fails to share. That is exactly what this ticket's "at minimum it should not be a hard provisioning failure" forbids.

    Fixed on the branch by targeting /var/tmp/devlaunch-pixi instead: /var/tmp is drwxrwxrwt in stock images, so an unmounted target stays writable (measured exit 0), it is still outside $HOME, and because it pre-exists nothing intermediate is created as root. Re-review before merge. #241

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions