gvisor/pkg/sentry/kernel/task_start.go

302 lines
8.5 KiB
Go
Raw Normal View History

// Copyright 2018 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package kernel
import (
"gvisor.dev/gvisor/pkg/abi/linux"
"gvisor.dev/gvisor/pkg/sentry/arch"
"gvisor.dev/gvisor/pkg/sentry/kernel/auth"
"gvisor.dev/gvisor/pkg/sentry/kernel/futex"
"gvisor.dev/gvisor/pkg/sentry/kernel/sched"
"gvisor.dev/gvisor/pkg/sentry/usage"
"gvisor.dev/gvisor/pkg/syserror"
)
// TaskConfig defines the configuration of a new Task (see below).
type TaskConfig struct {
// Kernel is the owning Kernel.
Kernel *Kernel
// Parent is the new task's parent. Parent may be nil.
Parent *Task
// If InheritParent is not nil, use InheritParent's parent as the new
// task's parent.
InheritParent *Task
// ThreadGroup is the ThreadGroup the new task belongs to.
ThreadGroup *ThreadGroup
// SignalMask is the new task's initial signal mask.
SignalMask linux.SignalSet
// TaskContext is the TaskContext of the new task. Ownership of the
// TaskContext is transferred to TaskSet.NewTask, whether or not it
// succeeds.
TaskContext *TaskContext
// FSContext is the FSContext of the new task. A reference must be held on
// FSContext, which is transferred to TaskSet.NewTask whether or not it
// succeeds.
FSContext *FSContext
// FDTable is the FDTableof the new task. A reference must be held on
// FDMap, which is transferred to TaskSet.NewTask whether or not it
// succeeds.
FDTable *FDTable
// Credentials is the Credentials of the new task.
Credentials *auth.Credentials
// Niceness is the niceness of the new task.
Niceness int
// If NetworkNamespaced is true, the new task should observe a non-root
// network namespace.
NetworkNamespaced bool
// AllowedCPUMask contains the cpus that this task can run on.
AllowedCPUMask sched.CPUSet
// UTSNamespace is the UTSNamespace of the new task.
UTSNamespace *UTSNamespace
// IPCNamespace is the IPCNamespace of the new task.
IPCNamespace *IPCNamespace
// AbstractSocketNamespace is the AbstractSocketNamespace of the new task.
AbstractSocketNamespace *AbstractSocketNamespace
// ContainerID is the container the new task belongs to.
ContainerID string
}
// NewTask creates a new task defined by cfg.
//
// NewTask does not start the returned task; the caller must call Task.Start.
func (ts *TaskSet) NewTask(cfg *TaskConfig) (*Task, error) {
t, err := ts.newTask(cfg)
if err != nil {
cfg.TaskContext.release()
cfg.FSContext.DecRef()
cfg.FDTable.DecRef()
return nil, err
}
return t, nil
}
// newTask is a helper for TaskSet.NewTask that only takes ownership of parts
// of cfg if it succeeds.
func (ts *TaskSet) newTask(cfg *TaskConfig) (*Task, error) {
tg := cfg.ThreadGroup
tc := cfg.TaskContext
t := &Task{
taskNode: taskNode{
tg: tg,
parent: cfg.Parent,
children: make(map[*Task]struct{}),
},
runState: (*runApp)(nil),
interruptChan: make(chan struct{}, 1),
signalMask: cfg.SignalMask,
signalStack: arch.SignalStack{Flags: arch.SignalStackFlagDisable},
tc: *tc,
fsContext: cfg.FSContext,
fdTable: cfg.FDTable,
p: cfg.Kernel.Platform.NewContext(),
k: cfg.Kernel,
ptraceTracees: make(map[*Task]struct{}),
allowedCPUMask: cfg.AllowedCPUMask.Copy(),
ioUsage: &usage.IO{},
niceness: cfg.Niceness,
netns: cfg.NetworkNamespaced,
utsns: cfg.UTSNamespace,
ipcns: cfg.IPCNamespace,
abstractSockets: cfg.AbstractSocketNamespace,
rseqCPU: -1,
futexWaiter: futex.NewWaiter(),
containerID: cfg.ContainerID,
}
t.creds.Store(cfg.Credentials)
t.endStopCond.L = &t.tg.signalHandlers.mu
t.ptraceTracer.Store((*Task)(nil))
// We don't construct t.blockingTimer until Task.run(); see that function
// for justification.
// Make the new task (and possibly thread group) visible to the rest of
// the system atomically.
ts.mu.Lock()
defer ts.mu.Unlock()
tg.signalHandlers.mu.Lock()
defer tg.signalHandlers.mu.Unlock()
if tg.exiting || tg.execing != nil {
// If the caller is in the same thread group, then what we return
// doesn't matter too much since the caller will exit before it returns
// to userspace. If the caller isn't in the same thread group, then
// we're in uncharted territory and can return whatever we want.
return nil, syserror.EINTR
}
if err := ts.assignTIDsLocked(t); err != nil {
return nil, err
}
// Below this point, newTask is expected not to fail (there is no rollback
// of assignTIDsLocked or any of the following).
// Logging on t's behalf will panic if t.logPrefix hasn't been initialized.
// This is the earliest point at which we can do so (since t now has thread
// IDs).
t.updateLogPrefixLocked()
if cfg.InheritParent != nil {
t.parent = cfg.InheritParent.parent
}
if t.parent != nil {
t.parent.children[t] = struct{}{}
}
if tg.leader == nil {
// New thread group.
tg.leader = t
if parentPG := tg.parentPG(); parentPG == nil {
tg.createSession()
} else {
// Inherit the process group and terminal.
parentPG.incRefWithParent(parentPG)
tg.processGroup = parentPG
tg.tty = t.parent.tg.tty
}
}
tg.tasks.PushBack(t)
tg.tasksCount++
tg.liveTasks++
tg.activeTasks++
// Propagate external TaskSet stops to the new task.
t.stopCount = ts.stopCount
t.mu.Lock()
defer t.mu.Unlock()
t.cpu = assignCPU(t.allowedCPUMask, ts.Root.tids[t])
t.startTime = t.k.RealtimeClock().Now()
return t, nil
}
// assignTIDsLocked ensures that new task t is visible in all PID namespaces in
// which it should be visible.
//
// Preconditions: ts.mu must be locked for writing.
func (ts *TaskSet) assignTIDsLocked(t *Task) error {
type allocatedTID struct {
ns *PIDNamespace
tid ThreadID
}
var allocatedTIDs []allocatedTID
for ns := t.tg.pidns; ns != nil; ns = ns.parent {
tid, err := ns.allocateTID()
if err != nil {
// Failure. Remove the tids we already allocated in descendant
// namespaces.
for _, a := range allocatedTIDs {
delete(a.ns.tasks, a.tid)
delete(a.ns.tids, t)
if t.tg.leader == nil {
delete(a.ns.tgids, t.tg)
}
}
return err
}
ns.tasks[tid] = t
ns.tids[t] = tid
if t.tg.leader == nil {
// New thread group.
ns.tgids[t.tg] = tid
}
allocatedTIDs = append(allocatedTIDs, allocatedTID{ns, tid})
}
return nil
}
// allocateTID returns an unused ThreadID from ns.
//
// Preconditions: ns.owner.mu must be locked for writing.
func (ns *PIDNamespace) allocateTID() (ThreadID, error) {
if ns.exiting {
// "In this case, a subsequent fork(2) into this PID namespace will
// fail with the error ENOMEM; it is not possible to create a new
// processes [sic] in a PID namespace whose init process has
// terminated." - pid_namespaces(7)
return 0, syserror.ENOMEM
}
tid := ns.last
for {
// Next.
tid++
if tid > TasksLimit {
tid = InitTID + 1
}
// Is it available?
Skip tid allocation which is using When leader of process group (session) exit, the process group ID (session ID) is holding by other processes in the process group, so the process group ID (session ID) can not be reused. If reusing the process group ID (seession ID) as new process group ID for new process, this will cause session create failed, and later runsc crash when access process group. The fix skip the tid if it is using by a process group (session) when allocating a new tid. We could easily reproduce the runsc crash follow these steps: 1. build test program, and run inside container int main(int argc, char *argv[]) { pid_t cpid, spid; cpid = fork(); if (cpid == -1) { perror("fork"); exit(EXIT_FAILURE); } if (cpid == 0) { pid_t sid = setsid(); printf("Start New Session %ld\n",sid); printf("Child PID %ld / PPID %ld / PGID %ld / SID %ld\n", getpid(),getppid(),getpgid(getpid()),getsid(getpid())); spid = fork(); if (spid == 0) { setpgid(getpid(), getpid()); printf("Set GrandSon as New Process Group\n"); printf("GrandSon PID %ld / PPID %ld / PGID %ld / SID %ld\n", getpid(),getppid(),getpgid(getpid()),getsid(getpid())); while(1) { usleep(1); } } sleep(3); exit(0); } else { exit(0); } return 0; } 2. build hello program int main(int argc, char *argv[]) { printf("Current PID is %ld\n", (long) getpid()); return 0; } 3. run script on host which run hello inside container, you can speed up the test with set TasksLimit as lower value. for (( i=0; i<65535; i++ )) do docker exec <container id> /test/hello done 4. when hello process reusing the process group of loop process, runsc will crash. panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x79f0c8] goroutine 612475 [running]: gvisor.googlesource.com/gvisor/pkg/sentry/kernel.(*ProcessGroup).decRefWithParent(0x0, 0x0) pkg/sentry/kernel/sessions.go:160 +0x78 gvisor.googlesource.com/gvisor/pkg/sentry/kernel.(*Task).exitNotifyLocked(0xc000663500, 0x0) pkg/sentry/kernel/task_exit.go:672 +0x2b7 gvisor.googlesource.com/gvisor/pkg/sentry/kernel.(*runExitNotify).execute(0x0, 0xc000663500, 0x0, 0x0) pkg/sentry/kernel/task_exit.go:542 +0xc4 gvisor.googlesource.com/gvisor/pkg/sentry/kernel.(*Task).run(0xc000663500, 0xc) pkg/sentry/kernel/task_run.go:91 +0x194 created by gvisor.googlesource.com/gvisor/pkg/sentry/kernel.(*Task).Start pkg/sentry/kernel/task_start.go:286 +0xfe
2019-06-12 13:13:13 +00:00
tidInUse := func() bool {
if _, ok := ns.tasks[tid]; ok {
return true
}
if _, ok := ns.processGroups[ProcessGroupID(tid)]; ok {
return true
}
if _, ok := ns.sessions[SessionID(tid)]; ok {
return true
}
return false
}()
Skip tid allocation which is using When leader of process group (session) exit, the process group ID (session ID) is holding by other processes in the process group, so the process group ID (session ID) can not be reused. If reusing the process group ID (seession ID) as new process group ID for new process, this will cause session create failed, and later runsc crash when access process group. The fix skip the tid if it is using by a process group (session) when allocating a new tid. We could easily reproduce the runsc crash follow these steps: 1. build test program, and run inside container int main(int argc, char *argv[]) { pid_t cpid, spid; cpid = fork(); if (cpid == -1) { perror("fork"); exit(EXIT_FAILURE); } if (cpid == 0) { pid_t sid = setsid(); printf("Start New Session %ld\n",sid); printf("Child PID %ld / PPID %ld / PGID %ld / SID %ld\n", getpid(),getppid(),getpgid(getpid()),getsid(getpid())); spid = fork(); if (spid == 0) { setpgid(getpid(), getpid()); printf("Set GrandSon as New Process Group\n"); printf("GrandSon PID %ld / PPID %ld / PGID %ld / SID %ld\n", getpid(),getppid(),getpgid(getpid()),getsid(getpid())); while(1) { usleep(1); } } sleep(3); exit(0); } else { exit(0); } return 0; } 2. build hello program int main(int argc, char *argv[]) { printf("Current PID is %ld\n", (long) getpid()); return 0; } 3. run script on host which run hello inside container, you can speed up the test with set TasksLimit as lower value. for (( i=0; i<65535; i++ )) do docker exec <container id> /test/hello done 4. when hello process reusing the process group of loop process, runsc will crash. panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x79f0c8] goroutine 612475 [running]: gvisor.googlesource.com/gvisor/pkg/sentry/kernel.(*ProcessGroup).decRefWithParent(0x0, 0x0) pkg/sentry/kernel/sessions.go:160 +0x78 gvisor.googlesource.com/gvisor/pkg/sentry/kernel.(*Task).exitNotifyLocked(0xc000663500, 0x0) pkg/sentry/kernel/task_exit.go:672 +0x2b7 gvisor.googlesource.com/gvisor/pkg/sentry/kernel.(*runExitNotify).execute(0x0, 0xc000663500, 0x0, 0x0) pkg/sentry/kernel/task_exit.go:542 +0xc4 gvisor.googlesource.com/gvisor/pkg/sentry/kernel.(*Task).run(0xc000663500, 0xc) pkg/sentry/kernel/task_run.go:91 +0x194 created by gvisor.googlesource.com/gvisor/pkg/sentry/kernel.(*Task).Start pkg/sentry/kernel/task_start.go:286 +0xfe
2019-06-12 13:13:13 +00:00
if !tidInUse {
ns.last = tid
return tid, nil
}
// Did we do a full cycle?
if tid == ns.last {
// No tid available.
return 0, syserror.EAGAIN
}
}
}
// Start starts the task goroutine. Start must be called exactly once for each
// task returned by NewTask.
//
// 'tid' must be the task's TID in the root PID namespace and it's used for
// debugging purposes only (set as parameter to Task.run to make it visible
// in stack dumps).
func (t *Task) Start(tid ThreadID) {
// If the task was restored, it may be "starting" after having already exited.
if t.runState == nil {
return
}
t.goroutineStopped.Add(1)
t.tg.liveGoroutines.Add(1)
t.tg.pidns.owner.liveGoroutines.Add(1)
t.tg.pidns.owner.runningGoroutines.Add(1)
// Task is now running in system mode.
t.accountTaskGoroutineLeave(TaskGoroutineNonexistent)
// Use the task's TID in the root PID namespace to make it visible in stack dumps.
go t.run(uintptr(tid)) // S/R-SAFE: synchronizes with saving through stops
}