Skip to content

Commit c6fb814

Browse files
dzerikcongwang-mk
authored andcommitted
feat(go): Protection opt-out parity — AllowDegraded / Disable + ProtectionMinABI
Mirror the Protection opt-out from #71 (allow_degraded / disable per Landlock protection) in the Go SDK, matching the Python surface so a Go sandbox can run below the default ABI v6 floor by degrading or disabling the v6-only protections. - Protection type with the six C-ABI discriminants (FS_REFER..ABSTRACT_UNIX_SOCKET_SCOPE). - AllowDegraded / Disable Sandbox fields, wired through the builder (Disable applied last, so it wins when a protection is in both). - ProtectionMinABI exposes sandlock_protection_min_abi for capability checks. Tests: TestProtectionMinABI pins the discriminants against the live FFI (a wrong value would resolve elsewhere and fail); TestRunDegradedBelowV6 confines and runs a fully-degradable policy end to end, with a negative control asserting the default strict policy fails below v6 so the degraded success is attributable to the opt-out. Closes #108
1 parent 6e0de9d commit c6fb814

4 files changed

Lines changed: 110 additions & 4 deletions

File tree

go/README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ lightweight Linux process sandbox built on Landlock, seccomp-bpf, and seccomp
55
user notification. No root, no Docker, no namespaces.
66

77
The bindings bind the sandlock C ABI (`libsandlock_ffi`) via cgo, mirroring the
8-
Python SDK's `Sandbox` surface. **Linux only**; the runtime requires Linux
9-
6.12+ (Landlock ABI v6).
8+
Python SDK's `Sandbox` surface. **Linux only**; by default the runtime requires
9+
Linux 6.12+ (Landlock ABI v6), but the `AllowDegraded` / `Disable` fields let a
10+
sandbox run on older kernels by degrading or disabling the v6-only protections.
1011

1112
```go
1213
import sandlock "github.com/multikernel/sandlock/go"

go/sandbox.go

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
// notification. It binds the sandlock C ABI (libsandlock_ffi) via cgo and
44
// mirrors the Python SDK's Sandbox surface.
55
//
6-
// The bindings are Linux-only. The runtime requires Linux 6.12+ (Landlock
7-
// ABI v6); see the project README for the full kernel feature matrix.
6+
// The bindings are Linux-only. By default the runtime requires Linux 6.12+
7+
// (Landlock ABI v6); use the AllowDegraded or Disable fields to run on older
8+
// kernels by degrading or disabling the v6-only protections. See the project
9+
// README for the full kernel feature matrix.
810
//
911
// # Building
1012
//
@@ -150,6 +152,20 @@ type PolicyContext struct {
150152
// activity, so captured state should be synchronized when mutated.
151153
type PolicyFunc func(event SyscallEvent, ctx *PolicyContext) PolicyDecision
152154

155+
// Protection identifies a single Landlock protection whose enforcement
156+
// posture can be opted out of via the Sandbox AllowDegraded / Disable fields.
157+
// The values match the sandlock C ABI discriminants.
158+
type Protection uint32
159+
160+
const (
161+
ProtectionFSRefer Protection = 0 // file reparenting (Landlock ABI v2)
162+
ProtectionFSTruncate Protection = 1 // truncate(2) (ABI v3)
163+
ProtectionNetTCP Protection = 2 // TCP bind/connect (ABI v4)
164+
ProtectionFSIoctlDev Protection = 3 // device ioctl(2) (ABI v5)
165+
ProtectionSignalScope Protection = 4 // signal scoping (ABI v6)
166+
ProtectionAbstractUnixSocketScope Protection = 5 // abstract UNIX socket scoping (ABI v6)
167+
)
168+
153169
// Sandbox holds the policy configuration for confining a process. Every field
154170
// is optional; an unset field means "no restriction" unless documented
155171
// otherwise. sandlock's default syscall blocklist is always applied.
@@ -172,6 +188,20 @@ type Sandbox struct {
172188
// like a bind mount without kernel mounts or root.
173189
FSMount map[string]string
174190

191+
// Protection opt-out (Landlock per-protection posture).
192+
//
193+
// By default every protection is enforced strictly, which requires the
194+
// host kernel to support it (the highest floor is ABI v6); on an older
195+
// kernel a strict protection it cannot satisfy makes the build fail.
196+
// AllowDegraded marks protections to enforce where the host supports them
197+
// and silently skip otherwise; Disable turns them off entirely. Together
198+
// they let a sandbox run on a kernel below the default v6 floor.
199+
//
200+
// A protection listed in both fields is disabled: Disable is applied
201+
// last and takes precedence.
202+
AllowDegraded []Protection
203+
Disable []Protection
204+
175205
// Network.
176206
//
177207
// NetAllow entries are outbound endpoint rules. The bare form is TCP

go/sandlock_linux.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,14 @@ func (s *Sandbox) buildPolicy() (*C.sandlock_sandbox_t, error) {
242242
b = C.sandlock_sandbox_builder_port_remap(b, cbool(true))
243243
}
244244

245+
// Protection opt-out (Landlock per-protection posture).
246+
for _, p := range s.AllowDegraded {
247+
b = C.sandlock_sandbox_builder_allow_degraded(b, C.uint32_t(p))
248+
}
249+
for _, p := range s.Disable {
250+
b = C.sandlock_sandbox_builder_disable(b, C.uint32_t(p))
251+
}
252+
245253
// HTTP ACL.
246254
for _, r := range s.HTTPAllow {
247255
str(func(b *C.sandlock_builder_t, c *C.char) *C.sandlock_builder_t {
@@ -754,6 +762,13 @@ func LandlockABIVersion() int { return int(C.sandlock_landlock_abi_version()) }
754762
// MinLandlockABI returns the minimum Landlock ABI version this build requires.
755763
func MinLandlockABI() int { return int(C.sandlock_min_landlock_abi()) }
756764

765+
// ProtectionMinABI returns the minimum Landlock ABI version the host kernel
766+
// must support for the given protection to be available, or 0 for an unknown
767+
// protection value.
768+
func ProtectionMinABI(p Protection) int {
769+
return int(C.sandlock_protection_min_abi(C.uint32_t(p)))
770+
}
771+
757772
// SyscallNr resolves a syscall name (e.g. "openat") to its kernel syscall
758773
// number for the host architecture. It returns an error for names sandlock
759774
// cannot resolve (syscalls outside the set it filters or supervises).

go/sandlock_linux_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,3 +251,63 @@ func TestConfineRejectsSupervisorConfig(t *testing.T) {
251251
t.Logf("Confine rejected with: %v", err)
252252
}
253253
}
254+
255+
func TestProtectionMinABI(t *testing.T) {
256+
// Also pins the Protection discriminants: a wrong value would resolve to a
257+
// different protection (or 0 for unknown) and fail these expectations.
258+
cases := []struct {
259+
p sandlock.Protection
260+
want int
261+
}{
262+
{sandlock.ProtectionFSRefer, 2},
263+
{sandlock.ProtectionFSTruncate, 3},
264+
{sandlock.ProtectionNetTCP, 4},
265+
{sandlock.ProtectionFSIoctlDev, 5},
266+
{sandlock.ProtectionSignalScope, 6},
267+
{sandlock.ProtectionAbstractUnixSocketScope, 6},
268+
}
269+
for _, c := range cases {
270+
if got := sandlock.ProtectionMinABI(c.p); got != c.want {
271+
t.Errorf("ProtectionMinABI(%d) = %d, want %d", c.p, got, c.want)
272+
}
273+
}
274+
}
275+
276+
// TestRunDegradedBelowV6 exercises the Protection opt-out end to end: a
277+
// fully-degradable policy must build and confine on any Landlock-capable
278+
// kernel, degrading the protections the host cannot provide instead of failing
279+
// the build the way the default strict posture does below ABI v6. It runs on
280+
// the low-ABI CI image where the strict suite is skipped.
281+
func TestRunDegradedBelowV6(t *testing.T) {
282+
if sandlock.LandlockABIVersion() < 1 {
283+
t.Skip("Landlock unavailable on this host")
284+
}
285+
// Negative control: below the v6 floor the default strict policy must fail
286+
// to build, so the degraded run's success is attributable to the opt-out
287+
// rather than to the protections being satisfiable on this host anyway.
288+
// On a v6+ host strict would succeed, so the contrast only applies below v6.
289+
if sandlock.LandlockABIVersion() < 6 {
290+
strict := &sandlock.Sandbox{FSReadable: rootfs}
291+
if _, err := strict.Run(context.Background(), "echo", "ok"); err == nil {
292+
t.Fatalf("default strict policy unexpectedly succeeded on Landlock ABI v%d (< v6); the degraded assertion would prove nothing", sandlock.LandlockABIVersion())
293+
}
294+
}
295+
sb := &sandlock.Sandbox{
296+
FSReadable: rootfs,
297+
AllowDegraded: []sandlock.Protection{
298+
sandlock.ProtectionFSRefer,
299+
sandlock.ProtectionFSTruncate,
300+
sandlock.ProtectionNetTCP,
301+
sandlock.ProtectionFSIoctlDev,
302+
sandlock.ProtectionSignalScope,
303+
sandlock.ProtectionAbstractUnixSocketScope,
304+
},
305+
}
306+
res, err := sb.Run(context.Background(), "echo", "ok")
307+
if err != nil {
308+
t.Fatalf("Run: %v", err)
309+
}
310+
if !res.Success || strings.TrimSpace(string(res.Stdout)) != "ok" {
311+
t.Fatalf("degraded run failed: exit=%d stdout=%q stderr=%q", res.ExitCode, res.Stdout, res.Stderr)
312+
}
313+
}

0 commit comments

Comments
 (0)