mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
* feat: Add support for environment variable protection for sandbox * chore: Update dangerous env var list * fix: Split profiles for improved environment protection * fix: pipx sandbox profile separation * chore: Show sandbox scrub info on error exit * fix: Code review fixes * test: Add e2e for sandbox environment scrubbing
84 lines
3.1 KiB
Go
84 lines
3.1 KiB
Go
package runner
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os/exec"
|
|
|
|
"github.com/safedep/dry/usefulerror"
|
|
"github.com/safedep/pmg/errcodes"
|
|
"github.com/safedep/pmg/internal/proc"
|
|
"github.com/safedep/pmg/internal/pty"
|
|
)
|
|
|
|
// ChildExitError marks a transparent passthrough: the wrapped package manager
|
|
// exited on its own. PMG attributes nothing to itself and mirrors the exit code,
|
|
// so the package manager's own output stands and PMG stays invisible.
|
|
type ChildExitError struct {
|
|
Code int // child's exit code (or 128+signum for signal termination)
|
|
Signaled bool // true if terminated by a signal (Ctrl+C, SIGTERM, …)
|
|
PMName string // package manager name, for the dim one-liner
|
|
Scrubbed int // env vars scrubbed by the sandbox, for the dim hint
|
|
}
|
|
|
|
func (e *ChildExitError) Error() string {
|
|
return fmt.Sprintf("%s exited with code %d", e.PMName, e.Code)
|
|
}
|
|
func (e *ChildExitError) ExitCode() int { return e.Code }
|
|
func (e *ChildExitError) Transparent() bool { return true }
|
|
func (e *ChildExitError) IsSignaled() bool { return e.Signaled }
|
|
func (e *ChildExitError) ScrubbedEnvCount() int { return e.Scrubbed }
|
|
|
|
// classify turns a package-manager execution error into either a transparent
|
|
// child exit or a visible PMG error, and is the only place the fork lives. It is
|
|
// pure: sandbox denials are persisted separately by the caller and never make a
|
|
// child's own non-zero exit loud — a restrictive policy routinely denies benign
|
|
// operations and causation cannot be inferred from a denial. Only a failure on
|
|
// PMG's side of the boundary (the tool never produced an exit status) is loud.
|
|
func classify(err error, pmName string, scrubbedEnv int) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
|
|
code, signaled, resolved := extractExit(err)
|
|
return decideExit(err, code, signaled, resolved, pmName, scrubbedEnv)
|
|
}
|
|
|
|
// extractExit pulls the exit code and signal status from a process error.
|
|
// resolved is false when the child never produced a real exit status (e.g. the
|
|
// binary failed to launch), which must surface as a visible PMG error.
|
|
func extractExit(err error) (code int, signaled bool, resolved bool) {
|
|
var exitErr *exec.ExitError
|
|
if errors.As(err, &exitErr) {
|
|
if signum, sig := proc.SignalInfo(exitErr.Sys()); sig {
|
|
return 128 + signum, true, true
|
|
}
|
|
code = exitErr.ExitCode()
|
|
return code, false, code >= 0
|
|
}
|
|
|
|
var ptyErr *pty.ExitError
|
|
if errors.As(err, &ptyErr) {
|
|
return ptyErr.Code, ptyErr.Signaled, ptyErr.Signaled || ptyErr.Code >= 0
|
|
}
|
|
|
|
return -1, false, false
|
|
}
|
|
|
|
func decideExit(err error, code int, signaled, resolved bool, pmName string, scrubbedEnv int) error {
|
|
if !resolved {
|
|
return visibleExecError(err)
|
|
}
|
|
return &ChildExitError{Code: code, Signaled: signaled, PMName: pmName, Scrubbed: scrubbedEnv}
|
|
}
|
|
|
|
// visibleExecError is the loud error for a genuine PMG-side failure: the package
|
|
// manager never produced an exit status (e.g. the binary could not be launched).
|
|
func visibleExecError(err error) error {
|
|
return usefulerror.NewUsefulError().
|
|
WithCode(errcodes.PackageManagerExecutionFailed).
|
|
WithHumanError("Failed to execute package manager command").
|
|
WithHelp("Check the package manager command and its arguments").
|
|
Wrap(err)
|
|
}
|