-
Notifications
You must be signed in to change notification settings - Fork 4
fix(entrypoint): chown $DEVA_HOME itself after UID remap #507
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| * [2026-07-28] Home dir chown race: usermod aborts, $DEVA_HOME stays untraversable :FIX:ENTRYPOINT:RELIABILITY: | ||
|
|
||
| ** Tension | ||
| Fresh persistent container, credentials-file auth, latest image: | ||
|
|
||
| #+BEGIN_EXAMPLE | ||
| [deva] Starting Claude Code v2.1.220 | ||
| env: 'claude': Permission denied | ||
| #+END_EXAMPLE | ||
|
|
||
| Same launcher, same image family, eleven sibling containers running fine. | ||
| The 2026-01-08 devlog claims this exact symptom was fixed by the whitelist | ||
| chown (=7511464=). It wasn't — the whitelist fixed the subdirs and left a | ||
| hole one level up. | ||
|
|
||
| ** Observation | ||
| Inside the broken container: | ||
|
|
||
| - =claude.exe= is =-rwxr-xr-x= and every dir on the way down is 755. Not the binary. | ||
| - =namei -l= shows the one bad component: =/home/deva= itself is | ||
| =drwxr-x--- 1001:1001= while the remapped user is 501:20. Mode 750 comes | ||
| from Ubuntu noble's =useradd= default (=HOME_MODE 0750=); owner 1001 is the | ||
| build-time UID. The user cannot traverse into its own home, so =env= | ||
| reports EACCES on everything under it. | ||
| - Whitelist subdirs (=.npm-global=, =.local=, ...) ARE 501:20 — so | ||
| =setup_nonroot_user= ran and its chowns worked. Only the home dir itself | ||
| was left behind. | ||
| - Working siblings show =/home/deva= at 501:20. Same entrypoint bytes | ||
| (diffed baked scripts), same shadow-utils version, same home mounts. | ||
|
|
||
| Mechanism: =usermod -u= implicitly chowns the home tree, and shadow's | ||
| =chown_tree= recurses first, chowning the top-level dir LAST. The walk | ||
| crosses live host mounts inside home (=~/.claude= and friends, actively | ||
| written by concurrent sessions). Any transient mid-walk error aborts the | ||
| tree chown — usermod exits 12, passwd IS already updated — and the top | ||
| dir never gets chowned. Our code suppresses stderr and only reacts when | ||
| the UID itself failed to change. Reproduced the exact broken state by | ||
| recreating a container with the same home-mount set; one-shot runs with | ||
| idle mounts come out clean, which is why this stayed latent for months. | ||
|
|
||
| There is a second, benign variant: usermod fails outright (rc != 0, UID | ||
| unchanged). The entrypoint adapts =DEVA_UID= to reality and everything | ||
| stays self-consistent at 1001 — launches work. Only the half-success | ||
| (passwd updated, chown aborted) bricks the container. | ||
|
|
||
| ** Decision | ||
| Add one explicit non-recursive chown of =$DEVA_HOME= itself after the | ||
| usermod block, before the whitelist loop. It runs with the (possibly | ||
| adapted) =DEVA_UID=, so both usermod outcomes stay consistent. | ||
|
|
||
| Rejected: | ||
| - Restoring =chown -R "$DEVA_HOME"= (pre-=5807889= behavior): recursive | ||
| chown across host mounts is the thing we deliberately stopped doing. | ||
| - Adding =$DEVA_HOME= to the whitelist loop: the loop is =chown -R= per | ||
| entry; home needs exactly depth-0. | ||
| - Replacing usermod with passwd-file editing + targeted chowns: right | ||
| long-term answer (usermod's implicit tree walk over host mounts is both | ||
| the race and a hazard), but a bigger change than this fix needs. | ||
|
|
||
| ** Tradeoff | ||
| usermod still walks the whole home tree on every UID remap — wasted IO | ||
| and a lingering hazard of chowning through bind mounts. The fix makes the | ||
| outcome correct, not the mechanism. If launches ever get slow on big | ||
| mounted homes, kill the implicit walk (=usermod= → passwd edit) instead | ||
| of tuning around it. | ||
|
|
||
| ** Next | ||
| The fault-injection trick (stub =usermod= that updates passwd, skips the | ||
| chown, exits 12) is exactly a regression test waiting for a harness — | ||
| tests/ has no entrypoint coverage at all, which is why two "Permission | ||
| denied" regressions in this same function (=5807889=, now this) shipped. | ||
|
|
||
| ** Artifacts | ||
| - =docker-entrypoint.sh=: explicit =chown "$DEVA_UID:$DEVA_GID" "$DEVA_HOME"= | ||
| in =setup_nonroot_user=. | ||
| - Verified: patched entrypoint + stubbed usermod (passwd updated, chown | ||
| skipped, rc=12) → home 501:20, claude launches. Unpatched + same stub → | ||
| reproduces the failure. | ||
| - Bricked-container first aid: =docker exec <c> chown <uid>:<gid> /home/deva=. |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On a newly created persistent container,
deva.shstarts the detached PID 1 entrypoint, sleeps only 0.3 seconds, and then invokes this entrypoint again withdocker exec(deva.shlines 4233, 4259, and 4287). Sinceusermodupdates passwd before completing its potentially slow home-tree walk, the exec invocation can observecurrent_uid == DEVA_UID, skip this entire conditional, and reach the agent while PID 1 is still walking and/home/devaremains owned by the build UID. The same intermittentenv: 'claude': Permission deniedcan therefore survive this fix; run the non-recursive home-directory ownership repair on every invocation after any UID adaptation, rather than only when that invocation detects a UID change.Useful? React with 👍 / 👎.