fix: triage the unwritable config dir remedy by cause

The chown hint is only correct when another account created files
inside the current user's own home. When a leaked HOME or
XDG_CONFIG_HOME points at another user's home (e.g. sudo -u on GitHub
runners), following it would chown that user's directory and brick
their pmg instead. Classify the failure against the passwd home,
which the leaked environment cannot influence, and prescribe:

- dir inside own home: restore ownership with chown
- dir outside own home: fix the leaked environment, never chown
- explicit PMG_CONFIG_DIR: make it writable

Used by both the fatal event-log error and the doctor check, and the
docs troubleshooting now carries the same two-case triage.
This commit is contained in:
Sahilb315
2026-07-14 03:56:07 +05:30
parent de0fa41852
commit cd9b45b3bc
6 changed files with 100 additions and 12 deletions
+37
View File
@@ -7,6 +7,7 @@ import (
"os/user"
"path/filepath"
"runtime"
"strings"
"time"
_ "embed"
@@ -657,6 +658,42 @@ func rootCacheDir() (string, error) {
return filepath.Join(home, ".cache"), nil
}
// realUserHomeDir returns the current user's home from the passwd database,
// ignoring HOME and XDG_* env vars that may be leaked from another account.
// Overridable in tests.
var realUserHomeDir = func() (string, error) {
u, err := user.Current()
if err != nil {
return "", err
}
return u.HomeDir, nil
}
// UnwritableConfigDirRemedy returns actionable help for a per-user config or
// event-log directory the current user cannot write. The wrong remedy is
// harmful: chown-ing a directory that belongs to another account steals it and
// bricks that account instead, so chown is only suggested when the directory
// is inside the current user's real home.
func UnwritableConfigDirRemedy(dir string) string {
if os.Getenv(pmgConfigDirEnvKey) != "" {
return fmt.Sprintf("PMG_CONFIG_DIR points at %s; make it writable by your user", dir)
}
home, err := realUserHomeDir()
if err == nil && home != "" && !pathWithinDir(dir, home) {
return fmt.Sprintf(
"pmg resolved its config directory to %s, outside your home (%s): HOME or XDG_CONFIG_HOME leaked from another account (e.g. sudo -u). Fix the environment, e.g. export XDG_CONFIG_HOME=\"$HOME/.config\"; do not chown another user's directory",
dir, home)
}
return fmt.Sprintf("If a root or sudo run created it, restore ownership: sudo chown -R $(id -un) %s", dir)
}
func pathWithinDir(path, dir string) bool {
cleanPath, cleanDir := filepath.Clean(path), filepath.Clean(dir)
return cleanPath == cleanDir || strings.HasPrefix(cleanPath, cleanDir+string(os.PathSeparator))
}
// configDir computes the path to the config directory.
func configDir() (string, error) {
dir := os.Getenv(pmgConfigDirEnvKey)
+51
View File
@@ -0,0 +1,51 @@
package config
import (
"testing"
"github.com/stretchr/testify/assert"
)
func withRealUserHome(t *testing.T, home string) {
t.Helper()
orig := realUserHomeDir
realUserHomeDir = func() (string, error) { return home, nil }
t.Cleanup(func() { realUserHomeDir = orig })
}
func TestUnwritableConfigDirRemedy(t *testing.T) {
t.Run("dir inside real home suggests chown", func(t *testing.T) {
t.Setenv("PMG_CONFIG_DIR", "")
withRealUserHome(t, "/home/alice")
remedy := UnwritableConfigDirRemedy("/home/alice/.config/safedep/pmg")
assert.Contains(t, remedy, "sudo chown -R")
assert.Contains(t, remedy, "/home/alice/.config/safedep/pmg")
})
t.Run("dir outside real home blames leaked env, never suggests chown", func(t *testing.T) {
t.Setenv("PMG_CONFIG_DIR", "")
withRealUserHome(t, "/home/pmgtest")
remedy := UnwritableConfigDirRemedy("/home/runner/.config/safedep/pmg")
assert.Contains(t, remedy, "XDG_CONFIG_HOME")
assert.NotContains(t, remedy, "sudo chown")
})
t.Run("explicit PMG_CONFIG_DIR gets its own remedy", func(t *testing.T) {
t.Setenv("PMG_CONFIG_DIR", "/srv/pmg")
withRealUserHome(t, "/home/alice")
remedy := UnwritableConfigDirRemedy("/srv/pmg")
assert.Contains(t, remedy, "PMG_CONFIG_DIR")
assert.NotContains(t, remedy, "sudo chown")
})
t.Run("sibling dir with home prefix is outside home", func(t *testing.T) {
t.Setenv("PMG_CONFIG_DIR", "")
withRealUserHome(t, "/home/alice")
remedy := UnwritableConfigDirRemedy("/home/alice-evil/.config/safedep/pmg")
assert.NotContains(t, remedy, "sudo chown")
})
}