fix(config): resolve the home and writable dirs like the legacy CLI - #197
pjcdawkins wants to merge 7 commits into
Conversation
Go and PHP share the writable dir: Go now keeps its credentials there and cleans up the legacy CLI's files in it. Resolve it the same way: - The home dir comes from <PREFIX>HOME, then HOME, then USERPROFILE. On Windows HOME can differ from USERPROFILE, e.g. in MSYS2. - If the writable dir cannot be written, e.g. on an application container, use <temp dir>/<tmp_sub_dir>. Otherwise the migration could miss the legacy sessions, and logout could leave the legacy SSH certificate and API cache in place. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fall back to the temp dir only when the directory is not writable by permissions (access W_OK, or the read-only attribute on Windows), as PHP's Filesystem::canWrite does, rather than on any write failure. On a full disk or with Windows ACLs, Go and PHP otherwise chose different directories. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Warning
Changes suggested — 🟡 2 warnings · ⚪ 1 nitpick
🔍 Full review · 4 files reviewed
⚪ Nitpick
internal/config/dir_unix.go:1— The constraint//go:build !windowsalso selects plan9, js/wasm and wasip1, butgolang.org/x/sys/unixdoes not provideunix.Accessthere. The siblinginternal/config/alt/fs_unix.gouses//go:build unix.
Verification
canWritematches the legacyFilesystem::canWrite: it stops at the first existing ancestor and checks only that one, as the PHP while loop does.- The fallback condition matches legacy
getWritableUserDir: unwritable, or a non-directory at the path, both go to<tmp>/<tmp_sub_dir>. - The env var order in
HomeDir(<PREFIX>HOME,HOME,USERPROFILE) matches legacyConfig::getHomeDirectory. golang.org/x/sysis already a direct requirement in go.mod, so the newuniximport needs no module change.
The new internal/config/dir_test.go covers the env var order in HomeDir and two temp-fallback cases: a read-only home (skipped on Windows and as root) and a file in place of the directory. The existing TestFromYAML checks the normal writable path. No test covers the Windows read-only-attribute branch or a nonexistent home directory. These tests run under make test (go test ./...).
Review details
- Commit: c5ca72a
- Model: claude-opus-5-5
Review 1 of 10 for this pull request · View the full run
The writable dir can fall back to a shared /tmp, and the temp dir can be in one too, e.g. if XDG_CACHE_HOME=/tmp or on a read-only filesystem. Another user could create the directory first, then read or replace the CLI's state, credentials or PHP binary. On Unix, both directories must now be owned by the current user, as must any symlink to them. Group and other permissions are removed. Windows temporary directories are already per user. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
📋 PR Summary Changes how the Go config layer finds the home and writable user directories so it picks the same ones as the legacy PHP CLI. Changes
|
There was a problem hiding this comment.
Warning
Changes suggested — 🟡 1 warning · 2 still open (1 nitpick)
🔁 Incremental · 4 files reviewed
Outstanding from earlier reviews:
- 🟡 #4166327757 —
internal/config/dir.go:117: Go and PHP layers can choose different state directories for odd HOME values. —HomeDirstill returns the first non-empty variable as given: it does not check the directory exists and does not apply realpath, unlike the legacygetHomeDirectory. - ⚪
internal/config/dir_unix.go:1: Inconsistent build constraint with sibling file. —dir_unix.gois still//go:build !windows, and now also usessyscall.Stat_t, which plan9/wasm/wasip1 don't provide. (first raised)
Verification
checkPrivateDirrefuses a symlink not owned by the uid before following it, so a /tmp symlink planted by another user is rejected.- A pre-existing shared
/tmp/<tmp_sub_dir>owned by another user now makesWritableUserDirreturn an error, so the CLI does not write state into it. - The 0o077 check makes
checkPrivateDirchmod a directory the user owns back to 0700, and the 'shared directory is tightened' test case covers that.
The new table-driven TestCheckPrivateDir in internal/config/dir_unix_test.go covers ownership, symlinks, non-directories and the mode change. No test calls TempDir or WritableUserDir with a directory that is writable but owned by someone else.
Review 2 of 10 for this pull request · View the full run
… HOME The ownership check broke runs as root with a preserved HOME (sudo -E) or with an arbitrary UID, where the home directory belongs to another user. It now applies only when the parent is world-writable, e.g. /tmp, where another user could have created the directory. A symlink is followed and its target is checked in the same way. HomeDir now requires the directory to exist and returns its real path, like the legacy CLI's getHomeDirectory, so both layers agree on relative or symlinked values. The Unix file uses the "unix" build constraint, like internal/config/alt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Note
Reviewed — No blocking findings · 🔵 1 minor point
🔁 Incremental · 4 files reviewed
🔵 Minor point
internal/config/dir_unix.go:57—hasSharedParentonly looks at the parent's world-writable bit (0o002). A parent that is group-writable but not world-writable (for exampleTMPDIRor<PREFIX>TMPset to a setgid project directory with mode 2775) counts as private. In that casecheckPrivateDirskips both the ownership check and the 0700 tightening. Any member of that group can createupsun-cli-tmpthere in advance with mode 0777.TempDirandWritableUserDirthen accept that directory, so the cached phar/PHP binary andstate.jsonend up in a directory the other user controls. Before this commit, ownership was enforced no matter what the parent's mode was.
Verification
HomeDirnow fails for a missing or non-directory value and returns the realpath. That matches legacyConfig::getHomeDirectory, including the error text and therealpath() ?: $valuefallback.checkPrivateDirrecurses on the fully resolved target fromfilepath.EvalSymlinks. A secondLstattherefore cannot see a symlink again, so the recursion stops.- Run under
sudo -E, the~user/.cache/upsun-cli-tmpdirectory is no longer rejected, because~user/.cacheis not world-writable. A dir pre-created in/tmpby another uid still fails the ownership check.
The commit adds table tests for HomeDir (env-var order, a symlink, a missing dir, a relative path) and for checkPrivateDir (shared vs. private parents, symlinks, another uid). These tests run in the go test jobs in .github/workflows/ci.yml, on both the Ubuntu and Windows runners. No test covers a group-writable parent.
Review 3 of 10 for this pull request · View the full run
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Note
Reviewed — No new issues found · 1 still open
🔁 Incremental · 1 file reviewed
Outstanding from earlier reviews:
- 🔵
internal/config/dir_unix.go:61: Other members of the group can plant or read CLI files in shared-group temp dirs. — hasSharedParent still checks only the 0o002 bit. The new comment documents this, but any member of a group-writable parent's group can still pre-create the directory there, and it is accepted without an ownership check or chmod. (first raised)
Verification
- The new doc comment on
hasSharedParentmatches what the code does: it checks only the parent's world-writable bit (Perm()&0o002). - The
/tmpfallback inWritableUserDirstill goes throughensurePrivateDir, and that rejects a directory owned by someone else when the parent is world-writable, as/tmpis. HomeDirnow fails with "invalid environment variable" when the value is not an existing directory, and it returns the EvalSymlinks-resolved absolute path, as the legacy realpath does.
This push only adds a doc comment. dir_unix_test.go, added earlier in the PR, covers checkPrivateDir and hasSharedParent. No new test was needed for a comment, and none was added.
Review 4 of 10 for this pull request · View the full run
Split out of #195, which needs the Go and PHP layers to agree on these directories.
HomeDirchecks<PREFIX>HOME,HOME, thenUSERPROFILE, as the legacy CLI does. On Windows,HOMEcan differ fromUSERPROFILE, e.g. in MSYS2 or Cygwin.WritableUserDirfalls back to a temporary directory (<tmp>/<tmp_sub_dir>) if the directory in the home directory cannot be written, e.g. on an application container, or if a file is in its place.Filesystem::canWrite, so both choose the same directory, even on a full disk. On Windows this checks the read-only attribute.🤖 Generated with Claude Code