//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] -- // 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 }