diff --git a/config/config.go b/config/config.go index a5cfbc6..87eb2aa 100644 --- a/config/config.go +++ b/config/config.go @@ -658,6 +658,12 @@ func rootCacheDir() (string, error) { return filepath.Join(home, ".cache"), nil } +// Overridable in tests to exercise the passwd-unavailable fallback. +var ( + rootConfigDirResolver = rootConfigDir + rootCacheDirResolver = rootCacheDir +) + // 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. @@ -707,11 +713,15 @@ func configDir() (string, error) { } if configGeteuid() == 0 { - base, err := rootConfigDir() - if err != nil { - return "", err + if base, err := rootConfigDirResolver(); err == nil { + return filepath.Join(base, pmgDefaultHomeRelativePath), nil + } else { + // No resolvable root passwd entry (e.g. scratch containers, + // minimal chroots). Fall back to env-based resolution: without a + // passwd database there is no user switching, so the cross-user + // poisoning this branch prevents cannot occur. + log.Warnf("failed to resolve root home for config dir, using environment: %v", err) } - return filepath.Join(base, pmgDefaultHomeRelativePath), nil } userConfigDir, err := os.UserConfigDir() @@ -835,11 +845,12 @@ func cacheDir() (string, error) { return filepath.Join(baseDir, pmgDefaultHomeRelativePath), nil case "darwin", "linux": if configGeteuid() == 0 { - base, err := rootCacheDir() - if err != nil { - return "", err + if base, err := rootCacheDirResolver(); err == nil { + return filepath.Join(base, pmgDefaultHomeRelativePath), nil + } else { + // Same fallback rationale as configDir. + log.Warnf("failed to resolve root home for cache dir, using environment: %v", err) } - return filepath.Join(base, pmgDefaultHomeRelativePath), nil } userCacheDir, err := os.UserCacheDir() diff --git a/config/rootdir_test.go b/config/rootdir_test.go index 0df6b93..d23538b 100644 --- a/config/rootdir_test.go +++ b/config/rootdir_test.go @@ -78,3 +78,23 @@ func TestCacheDirAsNonRootUsesEnvHome(t *testing.T) { require.NoError(t, err) assert.Contains(t, dir, "/home/victim") } + +func TestRootDirsFallBackToEnvWhenPasswdUnavailable(t *testing.T) { + poisonUserEnv(t) + withEuid(t, 0) + + origConfig, origCache := rootConfigDirResolver, rootCacheDirResolver + rootConfigDirResolver = func() (string, error) { return "", assert.AnError } + rootCacheDirResolver = func() (string, error) { return "", assert.AnError } + t.Cleanup(func() { + rootConfigDirResolver, rootCacheDirResolver = origConfig, origCache + }) + + dir, err := configDir() + require.NoError(t, err) + assert.Contains(t, dir, "/home/victim") + + dir, err = cacheDir() + require.NoError(t, err) + assert.Contains(t, dir, "/home/victim") +} diff --git a/docs/system-install.md b/docs/system-install.md index af81184..ff47652 100644 --- a/docs/system-install.md +++ b/docs/system-install.md @@ -6,7 +6,9 @@ Use system install when one machine or image should protect every user account: sudo pmg setup install --system ``` -Requires Linux and root. Because every user's shims execute the PMG binary by its absolute path, `--system` validates it first: the binary must be **root-owned**, world-executable, and not writable by group or others, and it must sit in a **root-owned directory** that is not world-writable. Install PMG as root into a standard path such as `/usr/local/bin`; a user-local build (e.g. `~/go/bin/pmg`) is rejected. +**Requires Linux and root.** Install PMG as root into a standard system path such as `/usr/local/bin`. A user-local build (e.g. `~/go/bin/pmg`) is rejected. + +`--system` enforces this because every user's shims run the PMG binary by absolute path. Before installing, it checks that the binary is **root-owned**, world-executable, not group- or other-writable, and located in a **root-owned directory** that isn't world-writable. Per-user `pmg setup install` remains available and does not conflict with a system install. @@ -113,6 +115,8 @@ Shared policy lives under `/etc/safedep/pmg`. Runtime data stays per user: You can relocate these with `PMG_CONFIG_DIR` and `PMG_CACHE_DIR`. +When pmg runs as root (including via sudo), its per-user data goes under `/root`, regardless of any `HOME` preserved by sudo. Root never writes into another user's home. + The invoking user must be able to write their config directory. PMG records an event log there on each run and fails the command if it cannot (unless event logging is disabled in config). In Docker images, avoid creating `/home//.config/safedep` as root during the build. Either fix ownership for the runtime user, or set `PMG_CONFIG_DIR` to a writable location. diff --git a/main.go b/main.go index 816600a..89cbf60 100644 --- a/main.go +++ b/main.go @@ -235,7 +235,7 @@ func eventlogInitError(err error) error { } return usefulerror.NewUsefulError(). WithCode(errcodes.Lifecycle). - WithHumanError("failed to initialize event logging"). + WithHumanError(fmt.Sprintf("failed to initialize event logging: %v", err)). Wrap(err) }