Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,45 @@ http_file(
urls = ["https://raw.githubusercontent.com/SchemaStore/schemastore/166136b96a14f103a948053903e9339e63ad9170/src/schemas/json/github-workflow.json"],
)

# Statically-linked QEMU user-mode emulators.
QEMU_USER_EXPORTS = 'exports_files(["usr/bin/qemu-aarch64", "usr/bin/qemu-riscv64", "usr/bin/qemu-x86_64"])'

deb_data = use_repo_rule("//tools/bazeldefs:extensions/deb_data.bzl", "deb_data")

http_archive(
name = "qemu_user_amd64",
build_file_content = 'exports_files(["data.tar.xz"])',
sha256 = "ca6ede739327a20ae5a3498230c8a3a9a072c586e131bc6bcc1201eb1725e336",
type = "deb",
urls = [
"https://snapshot.debian.org/archive/debian/20260905T205002Z/pool/main/q/qemu/qemu-user_10.0.13%2Bds-0%2Bdeb13u1_amd64.deb",
"https://deb.debian.org/debian/pool/main/q/qemu/qemu-user_10.0.13%2Bds-0%2Bdeb13u1_amd64.deb",
],
)

deb_data(
name = "qemu_user_amd64_files",
build_file_content = QEMU_USER_EXPORTS,
data = "@qemu_user_amd64//:data.tar.xz",
)

http_archive(
name = "qemu_user_arm64",
build_file_content = 'exports_files(["data.tar.xz"])',
sha256 = "c73711af02b97cd5e2667e735d9c06890c57165517110ca4e646c95a7083158d",
type = "deb",
urls = [
"https://snapshot.debian.org/archive/debian/20260905T205002Z/pool/main/q/qemu/qemu-user_10.0.13%2Bds-0%2Bdeb13u1_arm64.deb",
"https://deb.debian.org/debian/pool/main/q/qemu/qemu-user_10.0.13%2Bds-0%2Bdeb13u1_arm64.deb",
],
)

deb_data(
name = "qemu_user_arm64_files",
build_file_content = QEMU_USER_EXPORTS,
data = "@qemu_user_arm64//:data.tar.xz",
)

# Root certificates.
#
# Note that the sha256 hash is omitted here intentionally. This should not be
Expand Down
12 changes: 12 additions & 0 deletions governance/licensing.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,15 @@ exceptions:
distributed with gVisor. The LLVM exception only relaxes
Apache-2.0's requirements, and the MIT match comes from third-party
notices embedded in LICENSE.TXT.
- dependency: qemu_user_amd64
license: GPL-2.0-only
exception_rationale: >-
QEMU user-mode emulators for x86_64 build machines, run as a build tool
to execute binaries built for the target architecture when
cross-compiling. QEMU is neither linked into nor distributed with gVisor.
- dependency: qemu_user_arm64
license: GPL-2.0-only
exception_rationale: >-
QEMU user-mode emulators for arm64 build machines, run as a build tool
to execute binaries built for the target architecture when
cross-compiling. QEMU is neither linked into nor distributed with gVisor.
1 change: 0 additions & 1 deletion pkg/seccomp/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ go_library(
"//pkg/abi/linux",
"//pkg/bpf",
"//pkg/log",
"//pkg/sync",
"//pkg/timing",
"@org_golang_x_sys//unix:go_default_library",
],
Expand Down
66 changes: 53 additions & 13 deletions pkg/seccomp/precompiledseccomp/defs.bzl
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Macro for precompiling seccomp-bpf programs."""

load("//tools:defs.bzl", "go_binary")
load("//tools:defs.bzl", "go_binary", "select_arch", "target_emulator")

def precompiled_seccomp_rules(
name,
Expand Down Expand Up @@ -114,6 +114,9 @@ def precompiled_seccomp_rules(
embedsrcs = [
":" + out + ".gen.lib.tmpl.go",
],
pure = True,
noasan = True,
race = "off",
)
if exclude_in_fastbuild:
go_binary(
Expand All @@ -127,30 +130,67 @@ def precompiled_seccomp_rules(

# This genrule actually runs the go_binary we just declared, and writes
# its output (containing the precompiled rules) to the desired `out` file.
out_cmd = "$(location :" + name + "_gen_bin) --package='" + out_package_name + "' --out=$@"
#
# The generator is a source (rather than a tool) of this genrule, so that
# it is built for the target platform rather than for the execution
# platform. If the execution platform's architecture differs from the
# target's (i.e. when cross-compiling), it is run under a user-mode
# emulator. If no emulator is configured for the target architecture, it
# is run directly, which works if the kernel is configured to run foreign
# binaries (binfmt_misc) and fails otherwise.
#
# Execution platform constraints are deliberately not used here: they would
# make cross-architecture configurations fail analysis wherever no
# execution platform for the target architecture is available.
emulator = target_emulator()
out_args = " --package='" + out_package_name + "' --out=$@"
run_gen_cmd = (
"GEN=$(location :" + name + "_gen_bin); " +
"RUN=; " +
"if [ -n \"$$TARGET_ARCH\" ] && [ \"$$(uname -m)\" != \"$$TARGET_ARCH\" ]; then " +
" RUN=\"$$EMULATOR\"; " +
"fi; " +
"$$RUN \"$$GEN\"" + out_args + " || { " +
" echo \"Failed to run $$GEN (built for $$TARGET_ARCH) on $$(uname -m)" +
" (emulator: $${RUN:-none}).\" >&2; " +
" exit 1; " +
"}"
)
run_gen_env = (
"TARGET_ARCH=" + select_arch(
amd64 = "x86_64",
arm64 = "aarch64",
riscv64 = "riscv64",
default = "",
) + "; " +
"EMULATOR='" + emulator.cmd + "'; "
)
if exclude_in_fastbuild:
native.genrule(
name = name,
outs = [out],
cmd = select({
":" + name + "_fastbuild_cond": (
"$(location :" + name + "_gen_stubbed_bin) --package='" + out_package_name + "' --out=$@"
),
"//conditions:default": out_cmd,
srcs = select({
":" + name + "_fastbuild_cond": [],
"//conditions:default": [":" + name + "_gen_bin"],
}),
# The stubbed generator's output does not depend on the
# architecture, so it is built for the execution platform.
cmd = run_gen_env + select({
":" + name + "_fastbuild_cond": "$(location :" + name + "_gen_stubbed_bin)" + out_args,
"//conditions:default": run_gen_cmd,
}),
tools = select({
":" + name + "_fastbuild_cond": [":" + name + "_gen_stubbed_bin"],
"//conditions:default": [":" + name + "_gen_bin"],
}),
"//conditions:default": [],
}) + emulator.tools,
tags = tags + ["requires-mem:16g"],
)
else:
native.genrule(
name = name,
outs = [out],
cmd = (
"$(location :" + name + "_gen_bin) --package='" + out_package_name + "' --out=$@"
),
tools = [":" + name + "_gen_bin"],
srcs = [":" + name + "_gen_bin"],
cmd = run_gen_env + run_gen_cmd,
tools = emulator.tools,
tags = tags + ["requires-mem:16g"],
)
4 changes: 4 additions & 0 deletions pkg/seccomp/precompiledseccomp/precompile_gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
_ "embed"
"fmt"
"os"
"runtime/debug"
"sort"
"strings"

Expand Down Expand Up @@ -56,6 +57,9 @@ var loadProgramsFn = example.PrecompiledPrograms // PROGRAMS_FUNC_THIS_IS_A_LOAD
func main() {
flag.Parse()

debug.SetGCPercent(1000)
debug.SetMemoryLimit(2 << 30)

// Get a sorted list of programs.
var programs []precompiledseccomp.Program
disabledAtBuildTime := loadProgramsFn == nil
Expand Down
10 changes: 10 additions & 0 deletions pkg/seccomp/precompiledseccomp/precompiledseccomp.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"encoding/binary"
"fmt"
"maps"
"runtime"
"sort"
"strings"

Expand Down Expand Up @@ -100,6 +101,15 @@ func (v Values) Copy() Values {
return v2
}

// maxParallelism is the maximum value returned by Parallelism.
const maxParallelism = 8

// Parallelism returns the number of programs that `PrecompiledPrograms`
// implementations should compile concurrently.
func Parallelism() int {
return min(runtime.GOMAXPROCS(0), maxParallelism)
}

// Precompile compiles a `seccomp.Program` with the given values.
// It supports the notion of "variables", which are named in `vars`.
// Variables are uint32s which are only known at runtime, and whose value
Expand Down
109 changes: 27 additions & 82 deletions pkg/seccomp/seccomp.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import (
"gvisor.dev/gvisor/pkg/abi/linux"
"gvisor.dev/gvisor/pkg/bpf"
"gvisor.dev/gvisor/pkg/log"
"gvisor.dev/gvisor/pkg/sync"
"gvisor.dev/gvisor/pkg/timing"
)

Expand All @@ -44,18 +43,12 @@ const (

// Install generates BPF code based on the set of syscalls provided. It only
// allows syscalls that conform to the specification. Syscalls that violate the
// specification will trigger RET_KILL_PROCESS. If RET_KILL_PROCESS is not
// supported, violations will trigger RET_TRAP instead. RET_KILL_THREAD is not
// used because it only kills the offending thread and often keeps the sentry
// specification will trigger RET_KILL_PROCESS. RET_KILL_THREAD is not used
// because it only kills the offending thread and often keeps the sentry
// hanging.
//
// denyRules describes forbidden syscalls. rules describes allowed syscalls.
// denyRules is executed before rules.
//
// Be aware that RET_TRAP sends SIGSYS to the process and it may be ignored,
// making it possible for the process to continue running after a violation.
// However, it will leave a SECCOMP audit event trail behind. In any case, the
// syscall is still blocked from executing.
func (p *Program) Install(timer *timing.Timer) error {
// *** DEBUG TIP ***
// If you suspect the Sentry is getting killed due to a seccomp violation,
Expand Down Expand Up @@ -98,7 +91,8 @@ type Action string

// Values for SeccompAction.
const (
// Default is the default action; see saneDefaultAction().
// Default is the default action: ProgramOptions.DefaultAction, or
// KillProcess if that is Default too.
Default Action = ""

// Allow the syscall.
Expand Down Expand Up @@ -144,31 +138,27 @@ func (a Action) String() string {
}

// bpf returns the corresponding BPF action for the given seccomp action.
// If the action is `Default`, `defaultAction` is called.
func (a Action) bpf(defaultAction func() (linux.BPFAction, error)) (linux.BPFAction, error) {
// If the action is `Default`, `defaultAction` is returned.
func (a Action) bpf(defaultAction linux.BPFAction) linux.BPFAction {
switch a {
case Default:
da, err := defaultAction()
if err != nil {
return 0, fmt.Errorf("failed to determine default seccomp action: %w", err)
}
return da, nil
return defaultAction
case Allow:
return linux.SECCOMP_RET_ALLOW, nil
return linux.SECCOMP_RET_ALLOW
case UserNotify:
return linux.SECCOMP_RET_USER_NOTIF, nil
return linux.SECCOMP_RET_USER_NOTIF
case Log:
return linux.SECCOMP_RET_LOG, nil
return linux.SECCOMP_RET_LOG
case Trap:
return linux.SECCOMP_RET_TRAP, nil
return linux.SECCOMP_RET_TRAP
case Trace:
return linux.SECCOMP_RET_TRACE, nil
return linux.SECCOMP_RET_TRACE
case ReturnError:
return linux.SECCOMP_RET_ERRNO, nil
return linux.SECCOMP_RET_ERRNO
case KillThread:
return linux.SECCOMP_RET_KILL_THREAD, nil
return linux.SECCOMP_RET_KILL_THREAD
case KillProcess:
return linux.SECCOMP_RET_KILL_PROCESS, nil
return linux.SECCOMP_RET_KILL_PROCESS
default:
colonIndex := strings.Index(string(a), ":")
if colonIndex == -1 {
Expand All @@ -179,40 +169,8 @@ func (a Action) bpf(defaultAction func() (linux.BPFAction, error)) (linux.BPFAct
if err != nil {
panic(fmt.Sprintf("failed to parse seccomp action: %v", err))
}
act, err := ins.bpf(defaultAction)
if err != nil {
return 0, err
}
return act.WithReturnCode(uint16(code)), nil
}
}

var (
// killProcessAvailableOnce is used to ensure that isKillProcessAvailable is
// only called once.
killProcessAvailableOnce sync.Once

// killProcessAvailable is true if SECCOMP_RET_KILL_PROCESS is available.
killProcessAvailable bool

// killProcessAvailableErr is the error obtained when trying to determine if
// SECCOMP_RET_KILL_PROCESS is available.
killProcessAvailableErr error
)

// saneDefaultAction returns a sane default for a failure to match
// a seccomp-bpf filter. Either kill the process, or trap.
func saneDefaultAction() (linux.BPFAction, error) {
killProcessAvailableOnce.Do(func() {
killProcessAvailable, killProcessAvailableErr = isKillProcessAvailable()
})
if killProcessAvailableErr != nil {
return 0, killProcessAvailableErr
}
if killProcessAvailable {
return linux.SECCOMP_RET_KILL_PROCESS, nil
return ins.bpf(defaultAction).WithReturnCode(uint16(code))
}
return linux.SECCOMP_RET_TRAP, nil
}

// RuleSet is a set of rules and associated action.
Expand Down Expand Up @@ -445,6 +403,12 @@ type ProgramOptions struct {
HotSyscalls []uintptr
}

// defaultBPFAction returns the BPF action that `Default` actions resolve to:
// `o.DefaultAction`, or SECCOMP_RET_KILL_PROCESS if that is `Default` too.
func (o ProgramOptions) defaultBPFAction() linux.BPFAction {
return o.DefaultAction.bpf(linux.SECCOMP_RET_KILL_PROCESS)
}

// BuildStats contains information about seccomp program generation.
type BuildStats struct {
// SizeBeforeOptimizations and SizeAfterOptimizations correspond to the
Expand Down Expand Up @@ -474,20 +438,10 @@ func (p *Program) Build() ([]bpf.Instruction, BuildStats, error) {
return nil, BuildStats{}, err
}

defaultActionFn := func() (linux.BPFAction, error) {
return p.Options.DefaultAction.bpf(saneDefaultAction)
}
defaultAction, err := defaultActionFn()
if err != nil {
return nil, BuildStats{}, err
}
defaultAction := p.Options.defaultBPFAction()
possibleActions := make(map[linux.BPFAction]struct{})
for _, ruleSet := range p.RuleSets {
action, err := ruleSet.Action.bpf(defaultActionFn)
if err != nil {
return nil, BuildStats{}, err
}
possibleActions[action] = struct{}{}
possibleActions[ruleSet.Action.bpf(defaultAction)] = struct{}{}
}

program := &syscallProgram{
Expand All @@ -513,11 +467,7 @@ func (p *Program) Build() ([]bpf.Instruction, BuildStats, error) {

// Label if the architecture didn't match:
program.Label(badArchLabel)
badArchAction, err := p.Options.BadArchAction.bpf(defaultActionFn)
if err != nil {
return nil, BuildStats{}, err
}
program.Ret(badArchAction)
program.Ret(p.Options.BadArchAction.bpf(defaultAction))

insns, err := program.program.Instructions()
if err != nil {
Expand Down Expand Up @@ -687,14 +637,9 @@ func orderRuleSets(rules []RuleSet, options ProgramOptions) (orderedRuleSets, ti
// Build a single map of per-syscall syscallRuleActions.
// We will split this map up later.
allSyscallRuleActions := make(map[uintptr][]syscallRuleAction)
defaultActionFn := func() (linux.BPFAction, error) {
return options.DefaultAction.bpf(saneDefaultAction)
}
defaultAction := options.defaultBPFAction()
for _, rs := range rules {
action, err := rs.Action.bpf(defaultActionFn)
if err != nil {
return orderedRuleSets{}, 0, err
}
action := rs.Action.bpf(defaultAction)
for sysno, rule := range rs.Rules.rules {
existing, found := allSyscallRuleActions[sysno]
if !found {
Expand Down
Loading
Loading