mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
fix: Landlock probe environment and autofallback to BW
This commit is contained in:
@@ -33,13 +33,17 @@ type landlockSandbox struct {
|
||||
}
|
||||
|
||||
// newLandlockSandbox creates a new Landlock sandbox instance after verifying
|
||||
// that both Landlock and seccomp user notification are available on the system.
|
||||
// that Landlock and the no-NNP seccomp shim path 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)
|
||||
}
|
||||
|
||||
if err := landlockShimProbe(); err != nil {
|
||||
return nil, fmt.Errorf("landlock shim 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)
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
//go:build linux
|
||||
|
||||
package platform
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
var landlockShimProbe = runLandlockShimProbe
|
||||
|
||||
// runLandlockShimProbe verifies that the installed pmg binary can perform the
|
||||
// same critical operation as the real Landlock shim: create a user namespace
|
||||
// and install a seccomp-notify filter without setting NO_NEW_PRIVS.
|
||||
func runLandlockShimProbe() error {
|
||||
selfExe, err := os.Executable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolve self exe: %w", err)
|
||||
}
|
||||
|
||||
cmd := exec.Command(selfExe, "__landlock_probe")
|
||||
uid := os.Getuid()
|
||||
gid := os.Getgid()
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Cloneflags: syscall.CLONE_NEWUSER,
|
||||
UidMappings: []syscall.SysProcIDMap{
|
||||
{ContainerID: 0, HostID: uid, Size: 1},
|
||||
},
|
||||
GidMappings: []syscall.SysProcIDMap{
|
||||
{ContainerID: 0, HostID: gid, Size: 1},
|
||||
},
|
||||
GidMappingsEnableSetgroups: false,
|
||||
}
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
msg := strings.TrimSpace(string(output))
|
||||
if msg == "" {
|
||||
return fmt.Errorf("run shim probe: %w", err)
|
||||
}
|
||||
return fmt.Errorf("run shim probe: %w: %s", err, msg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RunLandlockProbe is the hidden self-reexec entry point used by
|
||||
// runLandlockShimProbe. It intentionally reuses shimInstallSeccomp so the probe
|
||||
// stays coupled to the exact no-NNP seccomp path the real shim needs.
|
||||
func RunLandlockProbe() error {
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
|
||||
notifyFd, err := shimInstallSeccomp(false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("probe: install seccomp: %w", err)
|
||||
}
|
||||
if err := unix.Close(notifyFd); err != nil {
|
||||
return fmt.Errorf("probe: close notify fd: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -10,6 +10,13 @@ import (
|
||||
"github.com/safedep/pmg/sandbox"
|
||||
)
|
||||
|
||||
var (
|
||||
landlockSandboxFactory = newLandlockSandbox
|
||||
bubblewrapSandboxFactory = func() (sandbox.Sandbox, error) {
|
||||
return newBubblewrapSandbox()
|
||||
}
|
||||
)
|
||||
|
||||
// NewSandbox creates a platform-specific sandbox instance for Linux.
|
||||
// Prefers Landlock (kernel 5.13+) with seccomp-notify for deny enforcement.
|
||||
// Falls back to Bubblewrap if Landlock or seccomp-notify is unavailable.
|
||||
@@ -20,18 +27,22 @@ func NewSandbox() (sandbox.Sandbox, error) {
|
||||
switch os.Getenv("PMG_SANDBOX_DRIVER") {
|
||||
case "bubblewrap":
|
||||
log.Debugf("PMG_SANDBOX_DRIVER=bubblewrap: forcing Bubblewrap sandbox")
|
||||
return newBubblewrapSandbox()
|
||||
return bubblewrapSandboxFactory()
|
||||
case "landlock":
|
||||
log.Debugf("PMG_SANDBOX_DRIVER=landlock: forcing Landlock sandbox")
|
||||
return newLandlockSandbox()
|
||||
return landlockSandboxFactory()
|
||||
}
|
||||
|
||||
sb, err := newLandlockSandbox()
|
||||
sb, err := landlockSandboxFactory()
|
||||
if err == nil {
|
||||
log.Debugf("Using Landlock sandbox driver (ABI V%d)", sb.(*landlockSandbox).abi.Version)
|
||||
if ll, ok := sb.(*landlockSandbox); ok {
|
||||
log.Debugf("Using Landlock sandbox driver (ABI V%d)", ll.abi.Version)
|
||||
} else {
|
||||
log.Debugf("Using Landlock sandbox driver")
|
||||
}
|
||||
return sb, nil
|
||||
}
|
||||
|
||||
log.Debugf("Landlock not available (%v), falling back to Bubblewrap", err)
|
||||
return newBubblewrapSandbox()
|
||||
return bubblewrapSandboxFactory()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
//go:build linux
|
||||
|
||||
package platform
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/safedep/pmg/sandbox"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type testSandbox struct {
|
||||
name string
|
||||
available bool
|
||||
}
|
||||
|
||||
func (s *testSandbox) Name() string {
|
||||
return s.name
|
||||
}
|
||||
|
||||
func (s *testSandbox) IsAvailable() bool {
|
||||
return s.available
|
||||
}
|
||||
|
||||
func (s *testSandbox) Execute(context.Context, *exec.Cmd, *sandbox.SandboxPolicy) (*sandbox.ExecutionResult, error) {
|
||||
return sandbox.NewExecutionResult(sandbox.WithExecutionResultSandbox(s)), nil
|
||||
}
|
||||
|
||||
func (s *testSandbox) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestNewSandbox_DefaultFallsBackToBubblewrapWhenLandlockUnavailable(t *testing.T) {
|
||||
restoreFactories := replaceSandboxFactories(t,
|
||||
func() (sandbox.Sandbox, error) {
|
||||
return nil, errors.New("landlock shim not available")
|
||||
},
|
||||
func() (sandbox.Sandbox, error) {
|
||||
return &testSandbox{name: "bubblewrap", available: true}, nil
|
||||
},
|
||||
)
|
||||
defer restoreFactories()
|
||||
t.Setenv("PMG_SANDBOX_DRIVER", "")
|
||||
|
||||
sb, err := NewSandbox()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "bubblewrap", sb.Name())
|
||||
}
|
||||
|
||||
func TestNewSandbox_DefaultUsesLandlockWhenAvailable(t *testing.T) {
|
||||
restoreFactories := replaceSandboxFactories(t,
|
||||
func() (sandbox.Sandbox, error) {
|
||||
return &testSandbox{name: "landlock", available: true}, nil
|
||||
},
|
||||
func() (sandbox.Sandbox, error) {
|
||||
return nil, errors.New("bubblewrap should not be used")
|
||||
},
|
||||
)
|
||||
defer restoreFactories()
|
||||
t.Setenv("PMG_SANDBOX_DRIVER", "")
|
||||
|
||||
sb, err := NewSandbox()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "landlock", sb.Name())
|
||||
}
|
||||
|
||||
func TestNewSandbox_ForcedLandlockDoesNotFallback(t *testing.T) {
|
||||
bubblewrapCalled := false
|
||||
restoreFactories := replaceSandboxFactories(t,
|
||||
func() (sandbox.Sandbox, error) {
|
||||
return nil, errors.New("landlock shim not available")
|
||||
},
|
||||
func() (sandbox.Sandbox, error) {
|
||||
bubblewrapCalled = true
|
||||
return &testSandbox{name: "bubblewrap", available: true}, nil
|
||||
},
|
||||
)
|
||||
defer restoreFactories()
|
||||
t.Setenv("PMG_SANDBOX_DRIVER", "landlock")
|
||||
|
||||
sb, err := NewSandbox()
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, sb)
|
||||
assert.False(t, bubblewrapCalled)
|
||||
}
|
||||
|
||||
func TestNewSandbox_ForcedBubblewrapSkipsLandlock(t *testing.T) {
|
||||
landlockCalled := false
|
||||
restoreFactories := replaceSandboxFactories(t,
|
||||
func() (sandbox.Sandbox, error) {
|
||||
landlockCalled = true
|
||||
return &testSandbox{name: "landlock", available: true}, nil
|
||||
},
|
||||
func() (sandbox.Sandbox, error) {
|
||||
return &testSandbox{name: "bubblewrap", available: true}, nil
|
||||
},
|
||||
)
|
||||
defer restoreFactories()
|
||||
t.Setenv("PMG_SANDBOX_DRIVER", "bubblewrap")
|
||||
|
||||
sb, err := NewSandbox()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "bubblewrap", sb.Name())
|
||||
assert.False(t, landlockCalled)
|
||||
}
|
||||
|
||||
func replaceSandboxFactories(
|
||||
t *testing.T,
|
||||
landlockFactory func() (sandbox.Sandbox, error),
|
||||
bubblewrapFactory func() (sandbox.Sandbox, error),
|
||||
) func() {
|
||||
t.Helper()
|
||||
|
||||
origLandlock := landlockSandboxFactory
|
||||
origBubblewrap := bubblewrapSandboxFactory
|
||||
landlockSandboxFactory = landlockFactory
|
||||
bubblewrapSandboxFactory = bubblewrapFactory
|
||||
|
||||
return func() {
|
||||
landlockSandboxFactory = origLandlock
|
||||
bubblewrapSandboxFactory = origBubblewrap
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user