bastion 0.55.3
ci / personal-data (push) Successful in 5s
ci / test (push) Successful in 3m51s
ci / fuzz (push) Successful in 2m55s

This commit is contained in:
Privatae
2026-10-25 23:16:45 +00:00
parent 77b7853ae1
commit 8822d5df85
136 changed files with 8638 additions and 1641 deletions
+95 -27
View File
@@ -224,6 +224,11 @@ type opReport struct {
// per change, in path order. An op_run apply is bound to it (ops.go opTerms.Changes).
ChangesSHA256 string `json:"changes_sha256"`
Committed []string `json:"committed,omitempty"`
// TasksCapped: a cap held the run's tasks, root's included, at
// opSandboxTasksMax. A root op where none could be held runs uncapped,
// and TasksNote says so and why.
TasksCapped bool `json:"tasks_capped,omitempty"`
TasksNote string `json:"tasks_note,omitempty"`
// An action op's apply (ops_verbs_linux.go): each verb as it ran, each
// check after it, and the receipts for what its program pushed.
Verbs []opVerbResult `json:"verbs,omitempty"`
@@ -421,18 +426,49 @@ func opRunSandboxed(opDir string, m *opManifest, mode, params string) (*opReport
limit := time.Duration(m.TimeLimit) * time.Second
ctx, cancel := context.WithTimeout(context.Background(), limit)
defer cancel()
cmd := exec.CommandContext(ctx, self, opSandboxSubcommand, specPath)
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWNS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNET | syscall.CLONE_NEWIPC | syscall.CLONE_NEWUTS,
Setpgid: true,
// A user op's tasks are held by RLIMIT_NPROC. Root is exempt from it, so
// a root op's are held by a scope of its own (ops_taskcap_linux.go), or
// by nothing where none can be made, and the report says which.
unit := ""
if spec.AsUser {
rep.TasksCapped = true
} else if why := opTasksScopeWhyNot(); why != "" {
rep.TasksNote = opTasksNotCapped(why)
} else {
unit = opScopeUnit(run)
}
var out *cappedBuffer
launch := func(unit string) error {
cmd := opSandboxCmd(ctx, self, specPath, unit)
cmd.Stdin = strings.NewReader(params)
out = &cappedBuffer{max: opOutputMaxBytes}
cmd.Stdout, cmd.Stderr = out, out
// systemd-run hands this environment on to the scope's process.
cmd.Env = []string{"PATH=/usr/sbin:/usr/bin:/sbin:/bin", opSandboxHelperEnv}
cmd.WaitDelay = 2 * time.Second // the pipes close when the namespace is gone; never wait past that
return opRunSandboxCmd(cmd)
}
cmd.Stdin = strings.NewReader(params)
out := &cappedBuffer{max: opOutputMaxBytes}
cmd.Stdout, cmd.Stderr = out, out
cmd.Env = []string{"PATH=/usr/sbin:/usr/bin:/sbin:/bin"}
cmd.WaitDelay = 2 * time.Second // the pipes close when the namespace is gone; never wait past that
start := time.Now()
runErr := cmd.Run()
runErr := launch(unit)
if unit != "" {
if ctx.Err() == context.DeadlineExceeded {
opKillScope(unit)
}
switch got, ran := opScopeTasks(run); {
case ran && got == strconv.Itoa(opSandboxScopeTasks):
rep.TasksCapped = true
case ran:
rep.TasksNote = opTasksNotCapped(fmt.Sprintf("its scope's pids.max read %q", truncate(got, 200)))
case ctx.Err() != nil:
rep.TasksNote = opTasksNotCapped("its scope did not start inside the time limit")
default:
// systemd-run could not make the scope, or the scope's process
// died before it recorded the cap: either way nothing ran, and the
// op runs as it does where there is no systemd.
rep.TasksNote = opTasksNotCapped("its scope never started the op: " + truncate(opLastLine(out.String()), 200))
runErr = launch("")
}
}
rep.Seconds = time.Since(start).Seconds()
rep.Output = out.String()
if ctx.Err() == context.DeadlineExceeded {
@@ -506,9 +542,16 @@ func opMatchDir(dir, like string) error {
return nil
}
// opUnprivilegedIdentityHook, when set, is the identity a user-privilege op
// runs as. Only tests set it.
var opUnprivilegedIdentityHook func() (uint32, uint32, error)
// opUnprivilegedIdentity is the session lane's user (session_unix.go): the
// dedicated account if it exists, else nobody.
func opUnprivilegedIdentity() (uint32, uint32, error) {
if opUnprivilegedIdentityHook != nil {
return opUnprivilegedIdentityHook()
}
var u *user.User
var err error
for _, name := range sessionUnprivUsers {
@@ -869,8 +912,10 @@ func opSandboxChildMain(arg string) int {
return 125
}
// Rlimits are inherited by the grandchild: no fork bombs, no giant files,
// a CPU cap beside the wall-clock one the parent enforces.
_ = unix.Setrlimit(unix.RLIMIT_NPROC, &unix.Rlimit{Cur: 64, Max: 64})
// a CPU cap beside the wall-clock one the parent enforces. The kernel
// does not hold root to RLIMIT_NPROC; a root op's tasks are capped by
// the scope it runs in (ops_taskcap_linux.go).
_ = unix.Setrlimit(unix.RLIMIT_NPROC, &unix.Rlimit{Cur: opSandboxTasksMax, Max: opSandboxTasksMax})
_ = unix.Setrlimit(unix.RLIMIT_FSIZE, &unix.Rlimit{Cur: opUpperMaxBytes, Max: opUpperMaxBytes})
_ = unix.Setrlimit(unix.RLIMIT_NOFILE, &unix.Rlimit{Cur: 256, Max: 256})
_ = unix.Setrlimit(unix.RLIMIT_CPU, &unix.Rlimit{Cur: uint64(spec.TimeLimit), Max: uint64(spec.TimeLimit)})
@@ -884,23 +929,46 @@ func opSandboxChildMain(arg string) int {
fmt.Fprintln(os.Stderr, "sandbox: no_new_privs:", err)
return 125
}
cmd := exec.Command(spec.Interpreter, opSandboxMount+"/run", "--mode", spec.Mode)
cmd.Dir = "/"
cmd.Env = []string{
"PATH=/usr/sbin:/usr/bin:/sbin:/bin",
"HOME=/tmp",
"TMPDIR=/tmp",
"LANG=C.UTF-8",
"PRIVATAE_OP_MODE=" + spec.Mode,
}
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
if spec.AsUser {
newCmd := func(ownUserNS bool) *exec.Cmd {
cmd := exec.Command(spec.Interpreter, opSandboxMount+"/run", "--mode", spec.Mode)
cmd.Dir = "/"
cmd.Env = []string{
"PATH=/usr/sbin:/usr/bin:/sbin:/bin",
"HOME=/tmp",
"TMPDIR=/tmp",
"LANG=C.UTF-8",
"PRIVATAE_OP_MODE=" + spec.Mode,
}
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
if !spec.AsUser {
cmd.Env = append(cmd.Env, "USER=root")
return cmd
}
cmd.Env = append(cmd.Env, "USER=privatae-op")
cmd.SysProcAttr = &syscall.SysProcAttr{Credential: &syscall.Credential{Uid: spec.UID, Gid: spec.GID, NoSetGroups: true}}
} else {
cmd.Env = append(cmd.Env, "USER=root")
attr := &syscall.SysProcAttr{Credential: &syscall.Credential{Uid: spec.UID, Gid: spec.GID, NoSetGroups: true}}
if ownUserNS {
// RLIMIT_NPROC counts every task of a user in its user namespace,
// so in the machine's own one the cap above would count whatever
// else runs as this user (nobody is shared with system daemons)
// and refuse the op on a busy machine. In a user namespace of its
// own the count is the op's tasks alone. The map is the identity:
// the op is the same user and sees every file's real owner.
attr.Cloneflags = syscall.CLONE_NEWUSER
attr.UidMappings = []syscall.SysProcIDMap{{ContainerID: 0, HostID: 0, Size: 1<<31 - 1}}
attr.GidMappings = []syscall.SysProcIDMap{{ContainerID: 0, HostID: 0, Size: 1<<31 - 1}}
}
cmd.SysProcAttr = attr
return cmd
}
if err := cmd.Run(); err != nil {
cmd := newCmd(true)
err = cmd.Run()
if spec.AsUser && err != nil && cmd.Process == nil {
// No user namespace here (not built in, or turned off): run the op
// as before, with the cap counting the user's other tasks too.
cmd = newCmd(false)
err = cmd.Run()
}
if err != nil {
var ee *exec.ExitError
if errors.As(err, &ee) {
if ws, ok := ee.Sys().(syscall.WaitStatus); ok && ws.Signaled() {