mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
fix: address system-install review findings
- shim: make system executable resolution injectable so tests pass under umask 002; skip the root-owner test when running as root - doctor: treat resolution into either the system or per-user shim dir as intercepted, and collapse the shim-in-PATH check to a single call site - setup: make remove (both --system and per-user) best-effort with errors.Join so one failed step no longer strands the other artifact - shim: allow a group-writable install parent dir (Debian/Ubuntu ship /usr/local/bin as root:staff 2775) while still rejecting world-writable and non-root-owned parents - audit: attribute cloud events to SUDO_USER when running under sudo - docs: drop the soft-fail event-logging claim (hard-fail is retained)
This commit is contained in:
+11
-9
@@ -1,6 +1,7 @@
|
||||
package shim
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -99,25 +100,26 @@ func (m *ShimManager) Install() error {
|
||||
}
|
||||
|
||||
func (m *ShimManager) Remove() error {
|
||||
// Best-effort: a failure removing the shim directory must not skip profile
|
||||
// and rc cleanup, otherwise a rerun is needed to fully uninstall.
|
||||
var errs []error
|
||||
if err := os.RemoveAll(m.config.BinDir); err != nil {
|
||||
return fmt.Errorf("failed to remove shim directory %s: %w", m.config.BinDir, err)
|
||||
errs = append(errs, fmt.Errorf("failed to remove shim directory %s: %w", m.config.BinDir, err))
|
||||
}
|
||||
|
||||
if m.config.ManageProfile {
|
||||
if err := removeSystemProfile(); err != nil {
|
||||
return fmt.Errorf("failed to remove system profile: %w", err)
|
||||
errs = append(errs, fmt.Errorf("failed to remove system profile: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
if m.config.SkipShellRc {
|
||||
return nil
|
||||
if !m.config.SkipShellRc {
|
||||
if err := m.removePathFromShells(); err != nil {
|
||||
errs = append(errs, fmt.Errorf("failed to clean shell configs: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
if err := m.removePathFromShells(); err != nil {
|
||||
return fmt.Errorf("failed to clean shell configs: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
func (m *ShimManager) IsInstalled() (bool, error) {
|
||||
|
||||
+19
-7
@@ -23,6 +23,10 @@ var (
|
||||
// systemExecutableOwnershipCheck requires root ownership of the binary and
|
||||
// its parent directory. Disabled in tests that cannot create root-owned files.
|
||||
systemExecutableOwnershipCheck = true
|
||||
// resolveExecutable resolves the running pmg binary for system install.
|
||||
// Overridable in tests so validation does not run against the go-build test
|
||||
// binary, which is group-writable under a 002 umask.
|
||||
resolveExecutable = currentExecutable
|
||||
)
|
||||
|
||||
// SystemBinDir returns the directory for system-wide PMG shims.
|
||||
@@ -57,7 +61,7 @@ func NewSystemShimManagerForRemove() (*ShimManager, error) {
|
||||
|
||||
func newSystemShimManager(validateExecutable bool) (*ShimManager, error) {
|
||||
aliasCfg := alias.DefaultConfig()
|
||||
pmgBin, err := currentExecutable()
|
||||
pmgBin, err := resolveExecutable()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -81,8 +85,8 @@ func newSystemShimManager(validateExecutable bool) (*ShimManager, error) {
|
||||
|
||||
// validateSystemExecutable rejects binaries unsafe for system-wide shims.
|
||||
// Shims hard-code this path, so the binary must be executable by all users,
|
||||
// not writable by group/others, and owned by root in a root-owned parent
|
||||
// directory that is not writable by group/others.
|
||||
// not writable by group/others, and owned by root in a root-owned, non-world-
|
||||
// writable parent.
|
||||
func validateSystemExecutable(path string) error {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
@@ -126,16 +130,24 @@ func requireRootOwnedPath(path string, info os.FileInfo) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// requireSafeParentDir validates only the immediate parent of the executable,
|
||||
// not the full chain up to /. It requires a root-owned, non-world-writable
|
||||
// parent so an unprivileged account cannot swap the shared binary that every
|
||||
// user's shims exec; a maliciously writable grandparent is out of scope.
|
||||
//
|
||||
// Group-writable is allowed deliberately: Debian/Ubuntu ship /usr/local/bin as
|
||||
// root:staff mode 2775, so rejecting group-writable would refuse the documented
|
||||
// install location out of the box. The tradeoff is that a member of the parent
|
||||
// directory's group can replace the binary — harden the directory (chmod g-w)
|
||||
// on multi-user hosts where that group is not trusted.
|
||||
func requireSafeParentDir(dir string) error {
|
||||
info, err := os.Stat(dir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to inspect directory %s: %w", dir, err)
|
||||
}
|
||||
|
||||
groupOrOtherWrite := os.FileMode(0o022)
|
||||
|
||||
if info.Mode().Perm()&groupOrOtherWrite != 0 {
|
||||
return fmt.Errorf("directory %s containing pmg executable is writable by group or others", dir)
|
||||
if info.Mode().Perm()&os.FileMode(0o002) != 0 {
|
||||
return fmt.Errorf("directory %s containing pmg executable is writable by others", dir)
|
||||
}
|
||||
|
||||
uid, ok := fileOwnerUID(info)
|
||||
|
||||
@@ -14,10 +14,19 @@ func useSystemPaths(t *testing.T, dir string) {
|
||||
systemBinDirOverride = filepath.Join(dir, "bin")
|
||||
systemProfilePathOverride = filepath.Join(dir, "profile.d", "pmg.sh")
|
||||
systemExecutableOwnershipCheck = false
|
||||
|
||||
// The go-build test binary is group-writable under a 002 umask, which the
|
||||
// executable validation rightly rejects. Point resolution at a crafted
|
||||
// 0755 binary so the manager validates a realistic path, not the harness.
|
||||
exe := filepath.Join(dir, "pmg")
|
||||
require.NoError(t, os.WriteFile(exe, []byte("#!/bin/sh\n"), 0o755))
|
||||
resolveExecutable = func() (string, error) { return exe, nil }
|
||||
|
||||
t.Cleanup(func() {
|
||||
systemBinDirOverride = ""
|
||||
systemProfilePathOverride = ""
|
||||
systemExecutableOwnershipCheck = true
|
||||
resolveExecutable = currentExecutable
|
||||
})
|
||||
}
|
||||
|
||||
@@ -142,6 +151,10 @@ func TestValidateSystemExecutableRejectsGroupWritable(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestValidateSystemExecutableRejectsNonRootOwner(t *testing.T) {
|
||||
if os.Geteuid() == 0 {
|
||||
t.Skip("running as root: temp file is root-owned, so the owner check passes")
|
||||
}
|
||||
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "pmg")
|
||||
require.NoError(t, os.WriteFile(path, []byte("binary"), 0o755))
|
||||
|
||||
Reference in New Issue
Block a user