Home
Why Sandlock
How It Works Use Cases Comparison Security Model
Docs
Documentation Home Getting Started CLI Reference Python SDK Sandbox Reference
Products
Overview Sandbox HTTP API Sandbox Scheduler
GitHub Schedule a Demo
Sandlock / Docs /Go SDK

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 path
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.

shell
# 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:

shell
$ 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

shell
$ cargo build --release -p sandlock-ffi
$ cd go && go build -tags sandlock_repo ./...
$ go test -tags sandlock_repo ./...

Quick start

go
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.

GroupFields
FilesystemFSReadable, FSWritable, FSDenied, Workdir, Cwd, Chroot, FSMount
NetworkNetAllow, NetDeny, NetAllowBind, NetDenyBind, PortRemap
HTTP ACLHTTPAllow, HTTPDeny, HTTPPorts, HTTPCAFile, HTTPKeyFile
ResourcesMaxMemory, MaxDisk, MaxProcesses, MaxCPU, MaxOpenFiles, CPUCores, NumCPUs, GPUDevices
SyscallsExtraAllowSyscalls, ExtraDenySyscalls
DeterminismRandomSeed, TimeStart, NoRandomizeMemory, NoHugePages, DeterministicDirs
EnvironmentCleanEnv, Env
MiscUID, GID, NoCoredump, Name
COW branchFSStorage, OnExit, OnError
Dynamic policyPolicyFn

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

go
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)
  • Run captures stdout and stderr and waits. A context deadline kills the process and returns a result with ExitCode == -1. Context cancellation without a deadline does not preempt a running child.
  • RunInteractive inherits the caller's stdio and returns the exit code.
  • DryRun runs against a temporary copy-on-write layer, reports the Changes it would have made, and discards them. Requires Workdir.
  • Spawn starts a process without waiting.
  • Popen is the streaming counterpart of Spawn.

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.

go
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

go
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

go
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
go
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

go
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.