bastion 0.55.3
This commit is contained in:
+95
-27
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user