mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
* feat(sandbox): ExecutionContext plumbing and fail-closed lockdown contract
Network lockdown needs the PMG proxy's address, which is only known at
spawn time. Thread an ExecutionContext from the proxy flow through the
runner and executor into every sandbox driver, and enforce the
network_via_proxy_only fail-closed contract: lockdown without a running
loopback proxy, or on a driver that cannot enforce it, is a hard error —
never a silent fallback to unrestricted network.
- sandbox.ExecutionContext{ProxyAddr} + 4-arg Sandbox.Execute
- sandbox.ValidateLockdown validates the proxy address (loopback only)
with usefulerror code SandboxRequiresProxy
- Seatbelt validates lockdown before translation (translation itself
lands next); bubblewrap and landlock reject lockdown as unsupported
until Linux enforcement is implemented
- executor.WithExecutionContext, runner.ExecuteOptions.SandboxProxyAddr,
proxy flow passes the live proxy address
- ApplySandbox also validates centrally before invoking the driver
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PqMU5GNBbQvQct9nxek1VS
* fix(sandbox): require numeric in-range proxy port in ValidateLockdown
The validated port string is embedded into generated sandbox profiles,
so service names, zero, and out-of-range ports are refused.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PqMU5GNBbQvQct9nxek1VS
* fix(sandbox): fail closed on Seatbelt lockdown until translation lands
A lockdown policy that passed proxy validation would silently receive
the pre-lockdown network rules from the translator. Reject it until the
lockdown profile translation is implemented, keeping the window between
plumbing and enforcement fail-closed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PqMU5GNBbQvQct9nxek1VS
* chore: review feedback — drop redundant comment, simplify stub help text
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PqMU5GNBbQvQct9nxek1VS
---------
Co-authored-by: Claude <noreply@anthropic.com>
121 lines
4.2 KiB
Go
121 lines
4.2 KiB
Go
//go:build linux
|
|
// +build linux
|
|
|
|
package platform
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os/exec"
|
|
|
|
"github.com/safedep/dry/log"
|
|
"github.com/safedep/dry/usefulerror"
|
|
"github.com/safedep/dry/utils"
|
|
"github.com/safedep/pmg/errcodes"
|
|
"github.com/safedep/pmg/sandbox"
|
|
)
|
|
|
|
// bubblewrapSandbox implements the Sandbox interface using Bubblewrap (bwrap) on Linux.
|
|
// Bubblewrap is a low-level unprivileged sandboxing tool that uses Linux namespaces
|
|
// to isolate processes with controlled access to filesystem, network, and IPC resources.
|
|
//
|
|
// This implementation follows the CLI-wrapper pattern (like Seatbelt on macOS):
|
|
// - Modifies the cmd in place by wrapping it with `bwrap` CLI
|
|
// - Returns ExecutionResult with executed=false
|
|
// - Caller must call cmd.Run() to execute the sandboxed command
|
|
type bubblewrapSandbox struct {
|
|
config *bubblewrapConfig
|
|
translator *bubblewrapPolicyTranslator
|
|
}
|
|
|
|
// newBubblewrapSandbox creates a new Bubblewrap sandbox instance with default configuration.
|
|
func newBubblewrapSandbox() (*bubblewrapSandbox, error) {
|
|
config := newDefaultBubblewrapConfig()
|
|
translator := newBubblewrapPolicyTranslator(config)
|
|
|
|
return &bubblewrapSandbox{
|
|
config: config,
|
|
translator: translator,
|
|
}, nil
|
|
}
|
|
|
|
// Execute prepares a command to run in the Bubblewrap sandbox with the given policy.
|
|
// It translates the PMG policy to bwrap CLI arguments and wraps the command execution.
|
|
//
|
|
// This implementation modifies the cmd in place and does NOT execute it.
|
|
// Returns ExecutionResult with executed=false, indicating the caller must run cmd.Run().
|
|
func (b *bubblewrapSandbox) Execute(ctx context.Context, cmd *exec.Cmd, policy *sandbox.SandboxPolicy, rt *sandbox.ExecutionContext) (*sandbox.ExecutionResult, error) {
|
|
if utils.SafelyGetValue(policy.NetworkViaProxyOnly) {
|
|
return nil, usefulerror.NewUsefulError().
|
|
WithCode(errcodes.UnsupportedPlatform).
|
|
WithHumanError("network_via_proxy_only is not yet supported on this platform").
|
|
WithHelp("Disable network_via_proxy_only for this profile.").
|
|
Wrap(fmt.Errorf("network_via_proxy_only is not yet supported on this platform (%s sandbox)", b.Name()))
|
|
}
|
|
|
|
bwrapPath, err := exec.LookPath("bwrap")
|
|
if err != nil {
|
|
return nil, usefulerror.NewUsefulError().
|
|
WithCode(errcodes.BubblewrapNotFound).
|
|
WithHumanError("Bubblewrap binary not found").
|
|
WithHelp("See more at: https://github.com/safedep/pmg/blob/main/docs/sandbox.md").
|
|
Wrap(fmt.Errorf("bubblewrap binary not found: %w", err))
|
|
}
|
|
|
|
bwrapArgs, err := b.translator.translate(policy)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to translate sandbox policy to bubblewrap arguments: %w", err)
|
|
}
|
|
|
|
log.Debugf("Bubblewrap arguments: %v", bwrapArgs)
|
|
|
|
originalPath := cmd.Path
|
|
originalArgs := cmd.Args
|
|
|
|
// Build bwrap command: bwrap [bwrap-args] -- <original-command> <original-args>
|
|
// The "--" separator is important to distinguish bwrap args from command args
|
|
cmd.Path = bwrapPath
|
|
cmd.Args = []string{"bwrap"}
|
|
|
|
// Add all translated bwrap arguments
|
|
cmd.Args = append(cmd.Args, bwrapArgs...)
|
|
|
|
// Add separator
|
|
cmd.Args = append(cmd.Args, "--")
|
|
|
|
// Add original command
|
|
cmd.Args = append(cmd.Args, originalPath)
|
|
|
|
// Add original arguments (skip argv[0] which is the command itself)
|
|
if len(originalArgs) > 1 {
|
|
cmd.Args = append(cmd.Args, originalArgs[1:]...)
|
|
}
|
|
|
|
log.Debugf("Sandboxed command: %s %v", cmd.Path, cmd.Args)
|
|
|
|
return sandbox.NewExecutionResult(sandbox.WithExecutionResultSandbox(b)), nil
|
|
}
|
|
|
|
// Name returns the name of this sandbox implementation.
|
|
func (b *bubblewrapSandbox) Name() sandbox.DriverName {
|
|
return sandbox.DriverBubblewrap
|
|
}
|
|
|
|
// IsAvailable returns true if bubblewrap (bwrap) is available on this system.
|
|
// Checks by attempting to locate the bwrap binary in PATH.
|
|
func (b *bubblewrapSandbox) IsAvailable() bool {
|
|
_, err := exec.LookPath("bwrap")
|
|
return err == nil
|
|
}
|
|
|
|
// Close cleans up any resources allocated by the sandbox.
|
|
// For Bubblewrap, there are no temporary files to clean up (unlike Seatbelt),
|
|
// since all configuration is passed via CLI arguments.
|
|
//
|
|
// This method is idempotent and safe to call multiple times.
|
|
func (b *bubblewrapSandbox) Close() error {
|
|
// Bubblewrap doesn't create temporary files like Seatbelt does,
|
|
// so there's nothing to clean up. All isolation is via CLI args.
|
|
return nil
|
|
}
|