//go:build linux package platform import ( "context" "encoding/json" "fmt" "io" "net" "os" "os/exec" "path/filepath" "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" ) // landlockSandbox implements the Sandbox interface using Landlock LSM on Linux. // This implementation follows the CLI-wrapper pattern (like Bubblewrap): // - Modifies the cmd in place by rewiring it to re-exec pmg with __landlock_sandbox_exec // - Passes the translated policy via a temp file (--policy-file) // - Passes an audit unix socket path (--audit-socket) for future audit event consumption // - Returns ExecutionResult with executed=false // - Caller must call cmd.Run() to execute the sandboxed command type landlockSandbox struct { abi *landlockABI // Cleanup state from last Execute() policyFile string socketPath string listener net.Listener } // newLandlockSandbox creates a new Landlock sandbox instance after verifying // that both Landlock and seccomp user notification are available on the system. func newLandlockSandbox() (sandbox.Sandbox, error) { abi, err := landlockDetectABI() if err != nil { return nil, fmt.Errorf("landlock not available: %w", err) } log.Debugf("Landlock ABI V%d detected (Refer=%v, Truncate=%v, Network=%v, IoctlDev=%v, Scoping=%v)", abi.Version, abi.HasRefer, abi.HasTruncate, abi.HasNetwork, abi.HasIoctlDev, abi.HasScoping) return &landlockSandbox{abi: abi}, nil } // Name returns the name of this sandbox implementation. func (s *landlockSandbox) Name() sandbox.DriverName { return sandbox.DriverLandlock } // IsAvailable returns true if Landlock is available and functional on this system. func (s *landlockSandbox) IsAvailable() bool { return s.abi != nil && s.abi.Version > 0 } // Close cleans up any resources allocated by the sandbox. // It closes the audit socket listener and removes temporary files. // This method is idempotent and safe to call multiple times. func (s *landlockSandbox) Close() error { if s.listener != nil { _ = s.listener.Close() s.listener = nil } if s.socketPath != "" { _ = os.Remove(s.socketPath) s.socketPath = "" } if s.policyFile != "" { _ = os.Remove(s.policyFile) s.policyFile = "" } return nil } // Execute prepares a command to run in the Landlock sandbox with the given policy. // It translates the PMG policy to a landlockExecPolicy, serializes it to a pipe, // and rewires the command to re-exec pmg with the __landlock_sandbox_exec subcommand. // // 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 (s *landlockSandbox) 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)", s.Name())) } execPolicy, err := landlockTranslatePolicy(policy, s.abi) if err != nil { return nil, fmt.Errorf("failed to translate policy: %w", err) } execPolicy.Command = cmd.Path if len(cmd.Args) > 1 { execPolicy.Args = cmd.Args[1:] } execPolicy.Env = cmd.Env policyFile, err := os.CreateTemp("", "pmg-landlock-policy-*.json") if err != nil { return nil, fmt.Errorf("failed to create policy temp file: %w", err) } if err := json.NewEncoder(policyFile).Encode(execPolicy); err != nil { _ = policyFile.Close() _ = os.Remove(policyFile.Name()) return nil, fmt.Errorf("failed to write policy to temp file: %w", err) } policyFilePath := policyFile.Name() _ = policyFile.Close() s.policyFile = policyFilePath socketPath := filepath.Join(os.TempDir(), fmt.Sprintf("pmg-landlock-audit-%d.sock", os.Getpid())) listener, err := net.Listen("unix", socketPath) if err != nil { _ = os.Remove(policyFilePath) return nil, fmt.Errorf("failed to create audit unix socket: %w", err) } s.socketPath = socketPath s.listener = listener go func() { conn, err := listener.Accept() if err != nil { return } if _, err := io.Copy(io.Discard, conn); err != nil { log.Warnf("audit socket drain: %v", err) } if err := conn.Close(); err != nil { log.Warnf("close audit conn: %v", err) } }() selfExe, err := os.Executable() if err != nil { if cerr := s.Close(); cerr != nil { log.Warnf("close landlock driver after self-exe lookup failure: %v", cerr) } return nil, fmt.Errorf("failed to get self executable path: %w", err) } originalPath := cmd.Path originalArgs := cmd.Args cmd.Path = selfExe cmd.Args = []string{ "pmg", "__landlock_sandbox_exec", "--policy-file", policyFilePath, "--audit-socket", socketPath, "--", originalPath, } if len(originalArgs) > 1 { cmd.Args = append(cmd.Args, originalArgs[1:]...) } log.Debugf("Landlock sandboxed command: %s %v", cmd.Path, cmd.Args) return sandbox.NewExecutionResult( sandbox.WithExecutionResultExecuted(false), sandbox.WithExecutionResultSandbox(s), ), nil }