Go SDK
cgo bindings over libsandlock_ffi, mirroring the Python SDK's surface. A Sandbox is a plain configuration struct with no runtime state, so it is safe to reuse and share across goroutines.
import sandlock "github.com/multikernel/sandlock/go"
Linux only. The runtime requires Linux 6.12 or later by default; the AllowDegraded and Disable fields let a sandbox run on older kernels by degrading or disabling the v6-only protections.
Building
cgo links against libsandlock_ffi, which the Rust workspace produces. There are two build modes.
Released mode: an installed library via pkg-config
The default build resolves the library and header through pkg-config, so the SDK works from another module once the native side is installed.
# installs libsandlock_ffi.so, sandlock.h, and sandlock.pc
$ sudo make install-go-lib
$ go get github.com/multikernel/sandlock/go
make install-go-lib honours PREFIX (default /usr/local) and DESTDIR. For a non-standard prefix, point pkg-config at it:
$ make install-go-lib PREFIX=$HOME/.local
$ export PKG_CONFIG_PATH=$HOME/.local/lib/pkgconfig
The installed sandlock.pc bakes an rpath to its libdir, so binaries find the shared library at runtime without LD_LIBRARY_PATH.
In-tree mode: build against a checkout
$ cargo build --release -p sandlock-ffi
$ cd go && go build -tags sandlock_repo ./...
$ go test -tags sandlock_repo ./...
Quick start
package main
import (
"context"
"fmt"
"log"
sandlock "github.com/multikernel/sandlock/go"
)
func main() {
sb := &sandlock.Sandbox{
FSReadable: []string{"/usr", "/lib", "/lib64", "/bin", "/etc"},
FSWritable: []string{"/tmp"},
}
res, err := sb.Run(context.Background(), "echo", "hello")
if err != nil {
log.Fatal(err)
}
fmt.Printf("exit=%d: %s", res.ExitCode, res.Stdout)
}
The Sandbox struct
Every field is optional, and an unset field means "no restriction" unless noted. Sandlock's default syscall blocklist is always applied. A Sandbox carries no runtime state, so it can be reused and shared across goroutines: Run, RunInteractive, and DryRun each build a fresh native policy.
| Group | Fields |
|---|---|
| Filesystem | FSReadable, FSWritable, FSDenied, Workdir, Cwd, Chroot, FSMount |
| Network | NetAllow, NetDeny, NetAllowBind, NetDenyBind, PortRemap |
| HTTP ACL | HTTPAllow, HTTPDeny, HTTPPorts, HTTPCAFile, HTTPKeyFile |
| Resources | MaxMemory, MaxDisk, MaxProcesses, MaxCPU, MaxOpenFiles, CPUCores, NumCPUs, GPUDevices |
| Syscalls | ExtraAllowSyscalls, ExtraDenySyscalls |
| Determinism | RandomSeed, TimeStart, NoRandomizeMemory, NoHugePages, DeterministicDirs |
| Environment | CleanEnv, Env |
| Misc | UID, GID, NoCoredump, Name |
| COW branch | FSStorage, OnExit, OnError |
| Dynamic policy | PolicyFn |
Network rule syntax
NetAllow entries follow Sandlock's rule grammar. A bare host:port is TCP and UDP ("api.openai.com:443", "github.com:22,443", ":53"); a target may be a host, an IP, or a CIDR ("10.0.0.0/8:443", "[2606:4700::/32]:443"); scheme prefixes opt other protocols in ("udp://1.1.1.1:53", "udp://*", "icmp://host", "icmp://*").
NetDeny is the inverse, a default-allow denylist restricted to IP and CIDR targets, mutually exclusive with NetAllow. NetAllowBind entries are comma-separated single ports or inclusive ranges ("8080", "3000-3010", "8080,9000-9005"), and NetDenyBind is its inverse. See the network model for the complete grammar.
Execution
func (s *Sandbox) Run(ctx context.Context, cmd ...string) (*Result, error)
func (s *Sandbox) RunInteractive(ctx context.Context, cmd ...string) (int, error)
func (s *Sandbox) DryRun(ctx context.Context, cmd ...string) (*DryRunResult, error)
func (s *Sandbox) Spawn(cmd ...string) (*Process, error)
func (s *Sandbox) Popen(stdio Stdio, cmd ...string) (*Process, error)
Runcaptures stdout and stderr and waits. A context deadline kills the process and returns a result withExitCode == -1. Context cancellation without a deadline does not preempt a running child.RunInteractiveinherits the caller's stdio and returns the exit code.DryRunruns against a temporary copy-on-write layer, reports theChangesit would have made, and discards them. RequiresWorkdir.Spawnstarts a process without waiting.Popenis the streaming counterpart ofSpawn.
Streaming with Popen
Each stream set to StdioPiped is handed back on the *Process as an *os.File you read or write while the child runs. The zero Stdio inherits all three, which is identical to Spawn.
p, _ := sb.Popen(sandlock.Stdio{
Stdin: sandlock.StdioPiped,
Stdout: sandlock.StdioPiped,
}, "cat")
defer p.Close()
p.Stdin.Write([]byte("hi\n"))
p.Stdin.Close() // EOF so cat exits
out, _ := io.ReadAll(p.Stdout) // "hi\n"
res, _ := p.Wait()
Ordering rules for piped streams. Close a piped Stdin before Wait, or let Wait close it, so a reader child sees EOF. Drain a piped Stdout or Stderr before Wait, or call Kill from another goroutine to interrupt a blocked Wait. A Popen'd process sends piped output to the Stdout and Stderr fields, so unlike Run, Result.Stdout and Result.Stderr are always empty.
Process lifecycle
func (p *Process) Pid() int
func (p *Process) Wait() (*Result, error)
func (p *Process) Pause() error // SIGSTOP to the process group
func (p *Process) Resume() error // SIGCONT
func (p *Process) Kill() error // SIGKILL
func (p *Process) Ports() (map[int]int, error) // virtual→real, with PortRemap
func (p *Process) Close() error // release the handle; kills if running
Dynamic policy
type PolicyFunc func(event SyscallEvent, ctx *PolicyContext) PolicyDecision
func Allow() PolicyDecision
func Deny() PolicyDecision
func Audit() PolicyDecision
func DenyWith(errnoValue int) PolicyDecision
func (e SyscallEvent) ArgvContains(sub string) bool
func (ctx *PolicyContext) RestrictNetwork(ips []string) error
func (ctx *PolicyContext) GrantNetwork(ips []string) error
func (ctx *PolicyContext) RestrictMaxMemory(bytes uint64)
func (ctx *PolicyContext) RestrictMaxProcesses(n uint32)
func (ctx *PolicyContext) RestrictPIDNetwork(pid uint32, ips []string) error
func (ctx *PolicyContext) DenyPath(path string) error
func (ctx *PolicyContext) AllowPath(path string) error
sb := &sandlock.Sandbox{
FSReadable: []string{"/usr", "/lib", "/lib64", "/bin", "/etc"},
PolicyFn: func(event sandlock.SyscallEvent, ctx *sandlock.PolicyContext) sandlock.PolicyDecision {
if event.Syscall == "execve" && event.ArgvContains("curl") {
return sandlock.Deny()
}
return sandlock.Allow()
},
}
Path strings are deliberately absent from events. Use the Landlock fields for static path policy and DenyPath or AllowPath for the dynamic hook. Argv is populated for execve and execveat events. The reasoning is on the dynamic policy page.
Confining the current process
func Confine(s *Sandbox) error
Applies the sandbox's Landlock filesystem rules to the current process, in place and irreversibly, with no fork and no exec. Only filesystem fields are honoured; configuration that needs a supervisor or a fresh child, such as seccomp, network rules, resource limits, or environment changes, is rejected rather than silently ignored. This is something the sandlock CLI cannot do.