mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
feat: Add support for Landlock based Sandbox for Linux (#238)
* feat: Initial implementation of landlock based sandbox driver * fix: Handle seccom probe failure * fix: Remove unnecessary seccomp probe * fix: Use file based policy load * fix: Keep bpf filter in memory * fix: Use TSYNC for seccom filter * fix: Use TSYNC for seccom filter * fix: Update landlock translator * fix: Landlock sandbox implementation * fix: Landlock + seccomp based sandboxing on Linux * fix: Misc fixes * fix: Cleanup sandbox files * fix: Handle mandatory deny API change post merge * fix: Landlock write access translation * chore: Fix linter issues * ci: Use /tmp for npm cache for landlock
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
//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/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() string {
|
||||
return "landlock"
|
||||
}
|
||||
|
||||
// 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) (*sandbox.ExecutionResult, error) {
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user