package platform import ( "bytes" "context" "errors" "os" "os/exec" "path/filepath" "time" "github.com/safedep/dry/log" "github.com/safedep/dry/utils" "github.com/safedep/pmg/sandbox" ) // canaryTimeout bounds the runtime of a single canary smoke test so a misconfigured // kernel cannot wedge the doctor command. const canaryTimeout = 15 * time.Second // canaryTargetPath is the file the canary attempts to read under a deny-all // policy. It is well-known on every supported host. const canaryTargetPath = "/etc/hostname" // canarySandboxFactory builds the sandbox driver under test. Separated from the // probe body so tests can stub it. type canarySandboxFactory func() (sandbox.Sandbox, error) // canaryCommandFactory builds the command the canary attempts to run. Stubbed // in tests; defaults to `cat /etc/hostname`. type canaryCommandFactory func(ctx context.Context) *exec.Cmd func defaultCanaryCommand(ctx context.Context) *exec.Cmd { return exec.CommandContext(ctx, "cat", canaryTargetPath) } // denyAllCanaryPolicy returns a minimal valid policy that denies read access to // the canary target. ValidateResolved requires at least one rule; the deny on // canaryTargetPath satisfies that and unambiguously asserts the sandbox is // blocking the read. func denyAllCanaryPolicy() *sandbox.SandboxPolicy { return &sandbox.SandboxPolicy{ Name: "pmg-canary", Description: "deny-all canary probe", PackageManagers: []string{"npm", "pip", "uv", "pypi"}, Filesystem: sandbox.FilesystemPolicy{ DenyRead: []string{canaryTargetPath}, }, AllowGitConfig: utils.PtrTo(false), AllowPTY: utils.PtrTo(false), AllowNetworkBind: utils.PtrTo(false), } } // runCanary executes the canary smoke test. On success, the sandbox prevents // the canary read and the command exits non-zero — that is the OK path. func runCanary(ctx context.Context, name string, driver sandbox.DriverName, factory canarySandboxFactory, cmdFactory canaryCommandFactory) sandbox.ProbeResult { ctx, cancel := context.WithTimeout(ctx, canaryTimeout) defer cancel() d := string(driver) sb, err := factory() if err != nil { return sandbox.ProbeResult{ Name: name, Status: sandbox.ProbeStatusFail, Summary: d + " sandbox could not be constructed", Detail: err.Error(), Fixes: []sandbox.ProbeFix{driverInstallFix(driver)}, } } defer func() { _ = sb.Close() }() if !sb.IsAvailable() { return sandbox.ProbeResult{ Name: name, Status: sandbox.ProbeStatusSkipped, Summary: d + " driver not available on this host", Fixes: []sandbox.ProbeFix{driverInstallFix(driver)}, } } cmd := cmdFactory(ctx) isCanaryRead := isCanaryReadCommand(cmd) var expected []byte if isCanaryRead { var readErr error expected, readErr = os.ReadFile(canaryTargetPath) if readErr != nil { log.Warnf("canary probe: failed to read baseline %s: %v", canaryTargetPath, readErr) } } var stdout, stderr bytes.Buffer if cmd.Stdout == nil { cmd.Stdout = &stdout } if cmd.Stderr == nil { cmd.Stderr = &stderr } policy := denyAllCanaryPolicy() result, err := sb.Execute(ctx, cmd, policy, nil) if err != nil { return sandbox.ProbeResult{ Name: name, Status: sandbox.ProbeStatusFail, Summary: d + " sandbox setup failed", Detail: err.Error(), Fixes: []sandbox.ProbeFix{driverInstallFix(driver)}, } } defer func() { _ = result.Close() }() if result.ShouldRun() { err = cmd.Run() } if err == nil { if isCanaryRead && len(expected) > 0 && !bytes.Equal(stdout.Bytes(), expected) { return sandbox.ProbeResult{ Name: name, Status: sandbox.ProbeStatusOK, Summary: d + " masked canary read", Detail: "the sandbox hid the contents of " + canaryTargetPath, } } return sandbox.ProbeResult{ Name: name, Status: sandbox.ProbeStatusFail, Summary: d + " did not block canary read of " + canaryTargetPath, Detail: "the sandbox executed `cat " + canaryTargetPath + "` without denial", Fixes: []sandbox.ProbeFix{{ Description: "Driver appears installed but not enforcing. Re-run with PMG_LOG_LEVEL=debug to inspect the translated policy.", }}, } } if errors.Is(err, context.DeadlineExceeded) { return sandbox.ProbeResult{ Name: name, Status: sandbox.ProbeStatusFail, Summary: d + " canary timed out", Detail: err.Error(), } } return sandbox.ProbeResult{ Name: name, Status: sandbox.ProbeStatusOK, Summary: d + " correctly blocked canary read", } } func isCanaryReadCommand(cmd *exec.Cmd) bool { if cmd == nil { return false } if len(cmd.Args) == 0 || filepath.Base(cmd.Args[0]) != "cat" { return false } for _, arg := range cmd.Args[1:] { if arg == canaryTargetPath { return true } } return false } func driverInstallFix(driver sandbox.DriverName) sandbox.ProbeFix { switch driver { case sandbox.DriverBubblewrap: return bubblewrapInstallFix() case sandbox.DriverLandlock: return sandbox.ProbeFix{ Description: "Upgrade to Linux 5.13+ for Landlock support.", Docs: "https://docs.kernel.org/userspace-api/landlock.html", } case sandbox.DriverSeatbelt: return sandbox.ProbeFix{ Description: "sandbox-exec ships with macOS — check SIP and PATH.", } } return sandbox.ProbeFix{Description: "Install the " + string(driver) + " sandbox driver."} } func bubblewrapInstallFix() sandbox.ProbeFix { return sandbox.ProbeFix{ Description: "Install bubblewrap using your distribution package manager and verify unprivileged user namespaces are enabled.", Docs: "https://github.com/containers/bubblewrap", } }